From e9478894a8aee185ab1f3cc34aefb8f87171a1f6 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:16:55 +0200 Subject: [PATCH 01/70] Avoid rotating pods for PGVERSION change outside of maintenance window (#3065) * Avoid rotating pods for PGVERSION change outside of maintenance window * Update docs --- docs/administrator.md | 10 +++++++--- pkg/cluster/sync.go | 11 ++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/administrator.md b/docs/administrator.md index 60db0f3f1..d18bb5349 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -65,7 +65,10 @@ the `PGVERSION` environment variable is set for the database pods. Since In-place major version upgrades can be configured to be executed by the operator with the `major_version_upgrade_mode` option. By default, it is enabled (mode: `manual`). In any case, altering the version in the manifest -will trigger a rolling update of pods to update the `PGVERSION` env variable. +will update the desired `PGVERSION`. If `maintenanceWindows` are configured, +major-version-related pod rotation is deferred until the next maintenance +window. Without maintenance windows, the operator will trigger a rolling +update of pods to apply the new `PGVERSION`. Spilo's [`configure_spilo`](https://github.com/zalando/spilo/blob/master/postgres-appliance/scripts/configure_spilo.py) script will notice the version mismatch but start the current version again. @@ -93,8 +96,9 @@ Thus, the `full` mode can create drift between desired and actual state. ### Upgrade during maintenance windows When `maintenanceWindows` are defined in the Postgres manifest the operator -will trigger a major version upgrade only during these periods. Make sure they -are at least twice as long as your configured `resync_period` to guarantee +will trigger major-version-related pod rotation and the major version upgrade +only during these periods. Make sure they are at least twice as long as your +configured `resync_period` to guarantee that operator actions can be triggered. ### Upgrade annotations diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index ffebd306c..3fa9e9783 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -41,6 +41,12 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { defer c.mu.Unlock() oldSpec := c.Postgresql + + if !c.isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) { + // do not apply any major version related changes yet + newSpec.Spec.PostgresqlParam.PgVersion = oldSpec.Spec.PostgresqlParam.PgVersion + } + c.setSpec(newSpec) defer func() { @@ -97,11 +103,6 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { } } - if !c.isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) { - // do not apply any major version related changes yet - newSpec.Spec.PostgresqlParam.PgVersion = oldSpec.Spec.PostgresqlParam.PgVersion - } - if err = c.syncStatefulSet(); err != nil { if !k8sutil.ResourceAlreadyExists(err) { err = fmt.Errorf("could not sync statefulsets: %v", err) From 39cc09ccaa1d5a91cca2464bcfb2c9988adff9d0 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Thu, 16 Apr 2026 17:13:18 +0200 Subject: [PATCH 02/70] feature toggle for using maintenance windows (#3074) * feature toggle for using maintenance windows --- .../crds/operatorconfigurations.yaml | 3 ++ charts/postgres-operator/values.yaml | 2 ++ docs/administrator.md | 10 +++--- docs/reference/cluster_manifest.md | 4 ++- docs/reference/operator_parameters.md | 3 ++ manifests/configmap.yaml | 1 + manifests/operatorconfiguration.crd.yaml | 3 ++ ...gresql-operator-default-configuration.yaml | 1 + pkg/apis/acid.zalan.do/v1/crds.go | 3 ++ .../v1/operator_configuration_type.go | 1 + .../acid.zalan.do/v1/zz_generated.deepcopy.go | 5 +++ pkg/cluster/util.go | 4 ++- pkg/cluster/util_test.go | 31 ++++++++++++++++++- pkg/controller/operator_config.go | 1 + pkg/util/config/config.go | 17 +++++----- 15 files changed, 73 insertions(+), 16 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 2c432a082..cb4b7a335 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -79,6 +79,9 @@ spec: enable_lazy_spilo_upgrade: type: boolean default: false + enable_maintenance_windows: + type: boolean + default: true enable_pgversion_env_var: type: boolean default: true diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 6d0161bfb..dfec76b6b 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -27,6 +27,8 @@ configGeneral: - "all" # update only the statefulsets without immediately doing the rolling update enable_lazy_spilo_upgrade: false + # toogle to use maintenance windows feature + enable_maintenance_windows: true # set the PGVERSION env var instead of providing the version via postgresql.bin_dir in SPILO_CONFIGURATION enable_pgversion_env_var: true # start any new database pod without limitations on shm memory diff --git a/docs/administrator.md b/docs/administrator.md index d18bb5349..b7880b183 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -95,11 +95,11 @@ Thus, the `full` mode can create drift between desired and actual state. ### Upgrade during maintenance windows -When `maintenanceWindows` are defined in the Postgres manifest the operator -will trigger major-version-related pod rotation and the major version upgrade -only during these periods. Make sure they are at least twice as long as your -configured `resync_period` to guarantee -that operator actions can be triggered. +When `maintenanceWindows` are defined in the Postgres manifest or in the global +config the operator will trigger major-version-related pod rotation and the +major version upgrade only during these periods. Make sure they are at least +twice as long as your configured `resync_period` to guarantee that operator +actions can be triggered. ### Upgrade annotations diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index ae23dabb9..fd0660f57 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -118,7 +118,9 @@ These parameters are grouped directly under the `spec` key in the manifest. a list which defines specific time frames when certain maintenance operations such as automatic major upgrades or master pod migration are allowed to happen. 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. + "Sat:00:00-04:00" for specific days, with all times in UTC. Note, when the + global config option `enable_maintenance_windows` is false, the specified + windows will be ignored. * **users** a map of usernames to user flags for the users that should be created in the diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index a62f67dfb..6dd775069 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -173,6 +173,9 @@ Those are top-level keys, containing both leaf keys and groups. the thresholds. The value must be `"true"` to be effective. The default is empty which means the feature is disabled. +* **enable_maintenance_windows** + toggle for using the maintenance windows feature. Default is `"true"`. + * **maintenance_windows** a list which defines specific time frames when certain maintenance operations such as automatic major upgrades or master pod migration are diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 1cf455e57..571a4171b 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -46,6 +46,7 @@ data: enable_ebs_gp3_migration_max_size: "1000" enable_init_containers: "true" enable_lazy_spilo_upgrade: "false" + enable_maintenance_windows: "true" enable_master_load_balancer: "false" enable_master_pooler_load_balancer: "false" enable_password_rotation: "false" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 03534cefb..3be545b65 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -77,6 +77,9 @@ spec: enable_lazy_spilo_upgrade: type: boolean default: false + enable_maintenance_windows: + type: boolean + default: true enable_pgversion_env_var: type: boolean default: true diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 9c54e4379..1c6a0e34a 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -8,6 +8,7 @@ configuration: # crd_categories: # - all # enable_lazy_spilo_upgrade: false + enable_maintenance_windows: true enable_pgversion_env_var: true # enable_shm_volume: true enable_spilo_wal_path_compat: false diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 46739e46d..3175f152a 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -105,6 +105,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "enable_lazy_spilo_upgrade": { Type: "boolean", }, + "enable_maintenance_windows": { + Type: "boolean", + }, "enable_shm_volume": { 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 bfad24b0d..453d618d3 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -266,6 +266,7 @@ type OperatorConfigurationData struct { Workers uint32 `json:"workers,omitempty"` ResyncPeriod Duration `json:"resync_period,omitempty"` RepairPeriod Duration `json:"repair_period,omitempty"` + EnableMaintenanceWindows *bool `json:"enable_maintenance_windows,omitempty"` MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"` SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"` ShmVolume *bool `json:"enable_shm_volume,omitempty"` 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 159a87f35..0fa4b1037 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -433,6 +433,11 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData *out = make([]string, len(*in)) copy(*out, *in) } + if in.EnableMaintenanceWindows != nil { + in, out := &in.EnableMaintenanceWindows, &out.EnableMaintenanceWindows + *out = new(bool) + **out = **in + } if in.MaintenanceWindows != nil { in, out := &in.MaintenanceWindows, &out.MaintenanceWindows *out = make([]MaintenanceWindow, len(*in)) diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 81e927d94..9c830129d 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -675,7 +675,9 @@ func isStandbyCluster(spec *acidv1.PostgresSpec) bool { } func (c *Cluster) isInMaintenanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool { - if len(specMaintenanceWindows) == 0 && len(c.OpConfig.MaintenanceWindows) == 0 { + ignoreMaintenanceWindows := c.OpConfig.EnableMaintenanceWindows != nil && !*c.OpConfig.EnableMaintenanceWindows + noWindowsDefined := len(specMaintenanceWindows) == 0 && len(c.OpConfig.MaintenanceWindows) == 0 + if noWindowsDefined || ignoreMaintenanceWindows { return true } now := time.Now() diff --git a/pkg/cluster/util_test.go b/pkg/cluster/util_test.go index ea3e81e89..8413ca396 100644 --- a/pkg/cluster/util_test.go +++ b/pkg/cluster/util_test.go @@ -660,6 +660,7 @@ func TestIsInMaintenanceWindow(t *testing.T) { cluster := New( Config{ OpConfig: config.Config{ + EnableMaintenanceWindows: util.True(), Resources: config.Resources{ ClusterLabels: map[string]string{"application": "spilo"}, ClusterNameLabel: "cluster-name", @@ -683,12 +684,27 @@ func TestIsInMaintenanceWindow(t *testing.T) { name string windows []acidv1.MaintenanceWindow configWindows []string + windowsFlag bool expected bool }{ { name: "no maintenance windows", windows: nil, configWindows: nil, + windowsFlag: true, + expected: true, + }, + { + name: "maintenance windows diabled", + windows: []acidv1.MaintenanceWindow{ + { + Everyday: true, + StartTime: mustParseTime("00:00"), + EndTime: mustParseTime("23:59"), + }, + }, + configWindows: nil, + windowsFlag: false, expected: true, }, { @@ -701,6 +717,7 @@ func TestIsInMaintenanceWindow(t *testing.T) { }, }, configWindows: nil, + windowsFlag: true, expected: true, }, { @@ -713,6 +730,7 @@ func TestIsInMaintenanceWindow(t *testing.T) { }, }, configWindows: nil, + windowsFlag: true, expected: true, }, { @@ -724,24 +742,35 @@ func TestIsInMaintenanceWindow(t *testing.T) { EndTime: mustParseTime(futureTimeEndFormatted), }, }, - expected: false, + windowsFlag: true, + expected: false, }, { name: "global maintenance windows with future interval time", windows: nil, configWindows: []string{fmt.Sprintf("%s-%s", futureTimeStartFormatted, futureTimeEndFormatted)}, + windowsFlag: true, expected: false, }, { name: "global maintenance windows all day", windows: nil, configWindows: []string{"00:00-02:00", "02:00-23:59"}, + windowsFlag: true, + expected: true, + }, + { + name: "global maintenance windows ignored", + windows: nil, + configWindows: []string{"00:00-02:00", "02:00-23:59"}, + windowsFlag: false, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + cluster.OpConfig.EnableMaintenanceWindows = &tt.windowsFlag cluster.OpConfig.MaintenanceWindows = tt.configWindows cluster.Spec.MaintenanceWindows = tt.windows if cluster.isInMaintenanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected { diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 7719a2939..0a458618b 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -51,6 +51,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.ShmVolume = util.CoalesceBool(fromCRD.ShmVolume, util.True()) result.SidecarImages = fromCRD.SidecarImages result.SidecarContainers = fromCRD.SidecarContainers + result.EnableMaintenanceWindows = util.CoalesceBool(fromCRD.EnableMaintenanceWindows, util.True()) if len(fromCRD.MaintenanceWindows) > 0 { result.MaintenanceWindows = make([]string, 0, len(fromCRD.MaintenanceWindows)) for _, window := range fromCRD.MaintenanceWindows { diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 468cf9328..914d7a180 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -173,14 +173,15 @@ type Config struct { LogicalBackup ConnectionPooler - 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 - MaintenanceWindows []string `name:"maintenance_windows"` - DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-18:4.1-p1"` - SidecarImages map[string]string `name:"sidecar_docker_images"` // deprecated in favour of SidecarContainers - SidecarContainers []v1.Container `name:"sidecars"` - PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"` + 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 + EnableMaintenanceWindows *bool `name:"enable_maintenance_windows" default:"true"` + MaintenanceWindows []string `name:"maintenance_windows"` + DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-18:4.1-p1"` + SidecarImages map[string]string `name:"sidecar_docker_images"` // deprecated in favour of SidecarContainers + SidecarContainers []v1.Container `name:"sidecars"` + PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"` // value of this string must be valid JSON or YAML; see initPodServiceAccount PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""` PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""` From 3af93363fd19408605f4ad5df9afb71d0cb0bdba Mon Sep 17 00:00:00 2001 From: Ali Algohary <71212887+alimelgohary@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:23:01 +0200 Subject: [PATCH 03/70] Fix JSON value of OPERATOR_UI_CONFIG (#3070) Remove excess comma from OPERATOR_UI_CONFIG. --- ui/manifests/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/manifests/deployment.yaml b/ui/manifests/deployment.yaml index ad41c38c7..e19a850b8 100644 --- a/ui/manifests/deployment.yaml +++ b/ui/manifests/deployment.yaml @@ -77,7 +77,7 @@ spec: "17", "16", "15", - "14", + "14" ] } # Exemple of settings to make snapshot view working in the ui when using AWS From bbf94324138580ec5d500614ef19907388bd9a15 Mon Sep 17 00:00:00 2001 From: DDD <58938832+dandeandean@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:59:08 -0400 Subject: [PATCH 04/70] Wasm target updates (#3068) * Updates Needed for WASM Target * switch to regular (instead of local) build flags * update codegen to match other scripts --------- Co-authored-by: Felix Kunde --- Makefile | 3 +++ go.mod | 4 ++-- go.sum | 4 ++++ hack/update-codegen.sh | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 02c9c73f5..b96d71939 100644 --- a/Makefile +++ b/Makefile @@ -78,6 +78,9 @@ $(GENERATED_CRDS): $(GENERATED) local: ${SOURCES} $(GENERATED_CRDS) CGO_ENABLED=${CGO_ENABLED} go build -o build/${BINARY} $(LOCAL_BUILD_FLAGS) -ldflags "$(LDFLAGS)" $(SOURCES) +wasm: ${SOURCES} $(GENERATED_CRDS) + GOOS=wasip1 GOARCH=wasm CGO_ENABLED=${CGO_ENABLED} go build -o build/${BINARY}.wasm ${BUILD_FLAGS} -ldflags "$(LDFLAGS)" $(SOURCES) + linux: ${SOURCES} $(GENERATED_CRDS) GOOS=linux GOARCH=amd64 CGO_ENABLED=${CGO_ENABLED} go build -o build/linux/${BINARY} ${BUILD_FLAGS} -ldflags "$(LDFLAGS)" $(SOURCES) diff --git a/go.mod b/go.mod index a25723a44..e0e0b1956 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/aws/aws-sdk-go v1.55.8 github.com/golang/mock v1.6.0 - github.com/lib/pq v1.10.9 + github.com/lib/pq v1.11.2 github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d github.com/pkg/errors v0.9.1 github.com/r3labs/diff v1.1.0 - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.45.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 463b37211..5b70c6899 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -113,6 +115,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 9d43bc512..1363c2786 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Copyright 2017 The Kubernetes Authors. # From 085a1a91e6c7f062b848078888d3f93b63914926 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:56:32 +0200 Subject: [PATCH 05/70] Bump werkzeug from 3.1.5 to 3.1.6 in /ui (#3076) Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/3.1.5...3.1.6) --- updated-dependencies: - dependency-name: werkzeug dependency-version: 3.1.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/requirements.txt b/ui/requirements.txt index 2e43ccb0e..ace18641d 100644 --- a/ui/requirements.txt +++ b/ui/requirements.txt @@ -11,4 +11,4 @@ kubernetes==11.0.0 python-json-logger==2.0.7 requests==2.32.4 stups-tokens>=1.1.19 -werkzeug==3.1.5 +werkzeug==3.1.6 From 0ba2147d733ed4c6cf4bc5d85232d1bddf0720d6 Mon Sep 17 00:00:00 2001 From: Zadkiel AHARONIAN Date: Thu, 23 Apr 2026 17:47:12 +0200 Subject: [PATCH 06/70] fix(logical-backup): wait for PG connectivity before running backup (#3069) * fix(logical-backup): wait for PG connectivity before running backup The backup script connects to the target PostgreSQL pod immediately after resolving its IP via the Kubernetes API. When NetworkPolicy is enforced via iptables, a newly-created pod's IP may not yet be present in the destination node's ingress allow lists, causing cross-node connections to be rejected until the next policy sync. This adds a pg_isready retry loop before the dump starts, with configurable retries and delay via LOGICAL_BACKUP_CONNECT_RETRIES (default: 10) and LOGICAL_BACKUP_CONNECT_RETRY_DELAY (default: 2s). Signed-off-by: Zadkiel AHARONIAN * docs: document LOGICAL_BACKUP_CONNECT_RETRIES and RETRY_DELAY env vars Document the new environment variables that control the pg_isready retry loop added in the previous commit. These are passed via the existing logical_backup_cronjob_environment_secret mechanism. Signed-off-by: Zadkiel AHARONIAN --------- Signed-off-by: Zadkiel AHARONIAN Co-authored-by: Ida Novindasari --- docs/reference/operator_parameters.md | 13 +++++++++++++ logical-backup/dump.sh | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 6dd775069..83f693acc 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -900,6 +900,19 @@ grouped under the `logical_backup` key. * **logical_backup_cronjob_environment_secret** Reference to a Kubernetes secret, which keys will be added as environment variables to the cronjob. Default: "" +The following environment variables can be passed to the logical backup +cronjob via `logical_backup_cronjob_environment_secret` to control +connectivity checks before the backup starts: + +* **LOGICAL_BACKUP_CONNECT_RETRIES** + Number of times to retry connecting to the target PostgreSQL pod before + giving up. This is useful when NetworkPolicy enforcement introduces a + short delay before a newly-created pod's IP is allowed through ingress + rules on the destination node. Default: "10" + +* **LOGICAL_BACKUP_CONNECT_RETRY_DELAY** + Delay in seconds between connectivity retries. Default: "2" + ## Debugging the operator Options to aid debugging of the operator itself. Grouped under the `debug` key. diff --git a/logical-backup/dump.sh b/logical-backup/dump.sh index a250670a6..7833de399 100755 --- a/logical-backup/dump.sh +++ b/logical-backup/dump.sh @@ -183,6 +183,25 @@ function get_master_pod { get_pods "labelSelector=${CLUSTER_NAME_LABEL}%3D${SCOPE},spilo-role%3Dmaster" | tee | head -n 1 } +# Wait for TCP connectivity to the target PostgreSQL pod. +# When NetworkPolicy is enforced via iptables, a newly-created pod's IP may not +# yet be present in the destination node's ingress allow lists, causing +# cross-node connections to be rejected until the next policy sync. +function wait_for_pg { + local retries=${LOGICAL_BACKUP_CONNECT_RETRIES:-10} + local delay=${LOGICAL_BACKUP_CONNECT_RETRY_DELAY:-2} + local i + for (( i=1; i<=retries; i++ )); do + if "$PG_BIN"/pg_isready -h "$PGHOST" -p "${PGPORT:-5432}" -q 2>/dev/null; then + return 0 + fi + echo "waiting for $PGHOST:${PGPORT:-5432} to become reachable (attempt $i/$retries)..." + sleep "$delay" + done + echo "ERROR: $PGHOST:${PGPORT:-5432} not reachable after $((retries * delay))s" + return 1 +} + CURRENT_NODENAME=$(get_current_pod | jq .items[].spec.nodeName --raw-output) export CURRENT_NODENAME @@ -197,6 +216,8 @@ for search in "${search_strategy[@]}"; do done +wait_for_pg + set -x if [ "$LOGICAL_BACKUP_PROVIDER" == "az" ]; then dump | compress > /tmp/azure-backup.sql.gz From 030c24f64e0504f6050245879fec7ad13d5282d8 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Thu, 23 Apr 2026 08:47:51 -0700 Subject: [PATCH 07/70] ui: honor AWS_ENDPOINT in read_basebackups S3 list/get (#3079) read_stored_clusters and read_versions build their S3 clients with endpoint_url=AWS_ENDPOINT, but read_basebackups used a bare client('s3') for both the list_objects_v2 paginator and the per-key get_object call. On MinIO / S3-compatible backends the list+get requests go to the default AWS endpoint, so the Backups tab renders cluster/version prefixes (picked up by the correctly-configured read_stored_clusters) but then returns empty base backup details (silently no hits against the real backend) (#3078). Build s3_client once per call with endpoint_url=AWS_ENDPOINT and reuse it for both the paginator and get_object. No behaviour change when AWS_ENDPOINT is unset; boto3 defaults to the AWS endpoint either way. Fixes #3078 Signed-off-by: SAY-5 Co-authored-by: SAY-5 Co-authored-by: Ida Novindasari --- ui/operator_ui/spiloutils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/operator_ui/spiloutils.py b/ui/operator_ui/spiloutils.py index 6a2f03bb2..8d2b73967 100644 --- a/ui/operator_ui/spiloutils.py +++ b/ui/operator_ui/spiloutils.py @@ -321,11 +321,18 @@ def read_basebackups( suffix = '' if uid == 'base' else '/' + uid backups = [] + # Reuse a single S3 client configured with AWS_ENDPOINT so MinIO / + # other S3-compatible backends are hit for list+get calls too. The + # previous plain client('s3') fell back to the default AWS endpoint + # and returned empty data against a custom endpoint; read_stored_clusters + # and read_versions already pass endpoint_url=AWS_ENDPOINT (#3078). + s3_client = client('s3', endpoint_url=AWS_ENDPOINT) + for vp in postgresql_versions: backup_prefix = f'{prefix}{pg_cluster}{suffix}/wal/{vp}/basebackups_005/' logger.info(f"{bucket}/{backup_prefix}") - paginator = client('s3').get_paginator('list_objects_v2') + paginator = s3_client.get_paginator('list_objects_v2') pages = paginator.paginate(Bucket=bucket, Prefix=backup_prefix) for page in pages: @@ -334,7 +341,7 @@ def read_basebackups( if not key.endswith("backup_stop_sentinel.json"): continue - response = client('s3').get_object(Bucket=bucket, Key=key) + response = s3_client.get_object(Bucket=bucket, Key=key) backup_info = loads(response["Body"].read().decode("utf-8")) last_modified = response["LastModified"].astimezone(timezone.utc).isoformat() From 27c969d14bda464c7a5e4000fc3a0cd35e6ab9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=A5rtensson?= Date: Fri, 24 Apr 2026 11:06:30 +0200 Subject: [PATCH 08/70] Set securityContext for backup container (#2117) Co-authored-by: Felix Kunde --- pkg/cluster/k8sres.go | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 724986dbc..7d51951ff 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -818,9 +818,6 @@ func (c *Cluster) generatePodTemplate( sidecarContainers []v1.Container, sharePgSocketWithSidecars *bool, tolerationsSpec *[]v1.Toleration, - spiloRunAsUser *int64, - spiloRunAsGroup *int64, - spiloFSGroup *int64, nodeAffinity *v1.Affinity, schedulerName *string, terminateGracePeriod int64, @@ -839,18 +836,22 @@ func (c *Cluster) generatePodTemplate( terminateGracePeriodSeconds := terminateGracePeriod containers := []v1.Container{*spiloContainer} containers = append(containers, sidecarContainers...) - securityContext := v1.PodSecurityContext{} + securityContext := v1.PodSecurityContext{ + RunAsUser: c.OpConfig.Resources.SpiloRunAsUser, + RunAsGroup: c.OpConfig.Resources.SpiloRunAsGroup, + FSGroup: c.OpConfig.Resources.SpiloFSGroup, + } - if spiloRunAsUser != nil { - securityContext.RunAsUser = spiloRunAsUser + if c.Spec.SpiloRunAsUser != nil { + securityContext.RunAsUser = c.Spec.SpiloRunAsUser } - if spiloRunAsGroup != nil { - securityContext.RunAsGroup = spiloRunAsGroup + if c.Spec.SpiloRunAsGroup != nil { + securityContext.RunAsGroup = c.Spec.SpiloRunAsGroup } - if spiloFSGroup != nil { - securityContext.FSGroup = spiloFSGroup + if c.Spec.SpiloFSGroup != nil { + securityContext.FSGroup = c.Spec.SpiloFSGroup } podSpec := v1.PodSpec{ @@ -1352,22 +1353,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef // pickup the docker image for the spilo container effectiveDockerImage := util.Coalesce(spec.DockerImage, c.OpConfig.DockerImage) - // determine the User, Group and FSGroup for the spilo pod - effectiveRunAsUser := c.OpConfig.Resources.SpiloRunAsUser - if spec.SpiloRunAsUser != nil { - effectiveRunAsUser = spec.SpiloRunAsUser - } - - effectiveRunAsGroup := c.OpConfig.Resources.SpiloRunAsGroup - if spec.SpiloRunAsGroup != nil { - effectiveRunAsGroup = spec.SpiloRunAsGroup - } - - effectiveFSGroup := c.OpConfig.Resources.SpiloFSGroup - if spec.SpiloFSGroup != nil { - effectiveFSGroup = spec.SpiloFSGroup - } - volumeMounts := generateVolumeMounts(spec.Volume) // configure TLS with a custom secret volume @@ -1485,9 +1470,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef sidecarContainers, c.OpConfig.SharePgSocketWithSidecars, &tolerationSpec, - effectiveRunAsUser, - effectiveRunAsGroup, - effectiveFSGroup, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity), spec.SchedulerName, int64(c.OpConfig.PodTerminateGracePeriod.Seconds()), @@ -2379,9 +2361,6 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { []v1.Container{}, util.False(), &tolerationsSpec, - nil, - nil, - nil, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil), nil, int64(c.OpConfig.PodTerminateGracePeriod.Seconds()), From 0ac28e3aad33bf0ad0c0ed26e799e712d623f3ce Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:23:54 +0200 Subject: [PATCH 09/70] Do not set aws-load-balancer-connection-idle-timeout by default (#3054) Co-authored-by: Felix Kunde --- docs/administrator.md | 6 ++-- e2e/tests/test_e2e.py | 2 -- pkg/cluster/cluster_test.go | 46 ++++++++----------------------- pkg/cluster/connection_pooler.go | 4 --- pkg/cluster/k8sres.go | 5 ---- pkg/util/constants/annotations.go | 2 -- 6 files changed, 14 insertions(+), 51 deletions(-) diff --git a/docs/administrator.md b/docs/administrator.md index b7880b183..e854775ce 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -891,15 +891,13 @@ cluster manifest. In the case any of these variables are omitted from the manifest, the operator configuration settings `enable_master_load_balancer` and `enable_replica_load_balancer` apply. Note that the operator settings affect all Postgresql services running in all namespaces watched by the operator. -If load balancing is enabled two default annotations will be applied to its -services: +If load balancing is enabled the following default annotation will be applied to +its services: - `external-dns.alpha.kubernetes.io/hostname` with the value defined by the operator configs `master_dns_name_format` and `replica_dns_name_format`. This value can't be overwritten. If any changing in its value is needed, it MUST be done changing the DNS format operator config parameters; and -- `service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout` with - a default value of "3600". There are multiple options to specify service annotations that will be merged with each other and override in the following order (where latter take diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 70145f3e4..8cadb98a7 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -724,14 +724,12 @@ def test_enable_disable_connection_pooler(self): master_annotations = { "external-dns.alpha.kubernetes.io/hostname": "acid-minimal-cluster-pooler.default.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", } self.eventuallyTrue(lambda: k8s.check_service_annotations( master_pooler_label+","+pooler_label, master_annotations), "Wrong annotations") replica_annotations = { "external-dns.alpha.kubernetes.io/hostname": "acid-minimal-cluster-pooler-repl.default.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", } self.eventuallyTrue(lambda: k8s.check_service_annotations( replica_pooler_label+","+pooler_label, replica_annotations), "Wrong annotations") diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index c7181dbbc..8046943d4 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -680,8 +680,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: make(map[string]string), serviceAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", }, }, { @@ -702,8 +701,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: make(map[string]string), serviceAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", }, }, { @@ -714,8 +712,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: make(map[string]string), serviceAnnotations: map[string]string{"foo": "bar"}, expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", "foo": "bar", }, }, @@ -737,8 +734,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: map[string]string{"foo": "bar"}, serviceAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", "foo": "bar", }, }, @@ -780,8 +776,7 @@ func TestServiceAnnotations(t *testing.T) { "external-dns.alpha.kubernetes.io/hostname": "wrong.external-dns-name.example.com", }, expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", }, }, { @@ -792,8 +787,7 @@ func TestServiceAnnotations(t *testing.T) { serviceAnnotations: make(map[string]string), operatorAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com", }, }, { @@ -835,8 +829,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: make(map[string]string), serviceAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", }, }, { @@ -857,8 +850,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: make(map[string]string), serviceAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", }, }, { @@ -869,8 +861,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: make(map[string]string), serviceAnnotations: map[string]string{"foo": "bar"}, expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", "foo": "bar", }, }, @@ -892,8 +883,7 @@ func TestServiceAnnotations(t *testing.T) { operatorAnnotations: map[string]string{"foo": "bar"}, serviceAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", "foo": "bar", }, }, @@ -935,8 +925,7 @@ func TestServiceAnnotations(t *testing.T) { "external-dns.alpha.kubernetes.io/hostname": "wrong.external-dns-name.example.com", }, expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", }, }, { @@ -947,8 +936,7 @@ func TestServiceAnnotations(t *testing.T) { serviceAnnotations: make(map[string]string), operatorAnnotations: make(map[string]string), expect: map[string]string{ - "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600", + "external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com", }, }, { @@ -1377,7 +1365,6 @@ 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"}, @@ -1406,7 +1393,6 @@ func TestCompareServices(t *testing.T) { current: 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"}, @@ -1414,7 +1400,6 @@ func TestCompareServices(t *testing.T) { new: 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"}, @@ -1426,7 +1411,6 @@ func TestCompareServices(t *testing.T) { current: 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"}, @@ -1434,7 +1418,6 @@ func TestCompareServices(t *testing.T) { 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"}, @@ -1447,7 +1430,6 @@ func TestCompareServices(t *testing.T) { 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"}, @@ -1455,7 +1437,6 @@ func TestCompareServices(t *testing.T) { new: newService( map[string]string{ constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, }, v1.ServiceTypeLoadBalancer, []string{"185.249.56.0/22"}, @@ -1468,7 +1449,6 @@ func TestCompareServices(t *testing.T) { 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"}, @@ -1476,7 +1456,6 @@ func TestCompareServices(t *testing.T) { new: newService( map[string]string{ constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, }, v1.ServiceTypeLoadBalancer, []string{}, @@ -1489,7 +1468,6 @@ func TestCompareServices(t *testing.T) { current: 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"}, diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index ac4ce67d8..e70eac56e 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -533,10 +533,6 @@ func (c *Cluster) generatePoolerServiceAnnotations(role PostgresRole, spec *acid annotations := c.getCustomServiceAnnotations(role, spec) if c.shouldCreateLoadBalancerForPoolerService(role, spec) { - // set ELB Timeout annotation with default value - if _, ok := annotations[constants.ElbTimeoutAnnotationName]; !ok { - annotations[constants.ElbTimeoutAnnotationName] = constants.ElbTimeoutAnnotationValue - } // -repl suffix will be added by replicaDNSName clusterNameWithPoolerSuffix := c.connectionPoolerName(Master) if role == Master { diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 7d51951ff..2eb867f06 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -2029,11 +2029,6 @@ func (c *Cluster) generateServiceAnnotations(role PostgresRole, spec *acidv1.Pos if c.shouldCreateLoadBalancerForService(role, spec) { dnsName := c.dnsName(role) - // Just set ELB Timeout annotation with default value, if it does not - // have a custom value - if _, ok := annotations[constants.ElbTimeoutAnnotationName]; !ok { - annotations[constants.ElbTimeoutAnnotationName] = constants.ElbTimeoutAnnotationValue - } // External DNS name annotation is not customizable annotations[constants.ZalandoDNSNameAnnotation] = dnsName } diff --git a/pkg/util/constants/annotations.go b/pkg/util/constants/annotations.go index fc5a84fa5..0330ddcb8 100644 --- a/pkg/util/constants/annotations.go +++ b/pkg/util/constants/annotations.go @@ -3,8 +3,6 @@ package constants // Names and values in Kubernetes annotation for services, statefulsets and volumes const ( ZalandoDNSNameAnnotation = "external-dns.alpha.kubernetes.io/hostname" - ElbTimeoutAnnotationName = "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout" - ElbTimeoutAnnotationValue = "3600" KubeIAmAnnotation = "iam.amazonaws.com/role" VolumeStorateProvisionerAnnotation = "pv.kubernetes.io/provisioned-by" PostgresqlControllerAnnotationKey = "acid.zalan.do/controller" From 688bbf1b9e4fd99edc637d2a0de9f7a507b3b1e6 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Tue, 28 Apr 2026 10:17:28 +0200 Subject: [PATCH 10/70] update standby check in pooler code (#3088) --- pkg/cluster/connection_pooler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index e70eac56e..336ffd4d9 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -908,7 +908,7 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look // in this case also do not forget to install lookup function // skip installation in standby clusters, since they are read-only - if !c.ConnectionPooler[role].LookupFunction && c.Spec.StandbyCluster == nil { + if !c.ConnectionPooler[role].LookupFunction && !isStandbyCluster(&newSpec.Spec) { connectionPooler := c.Spec.ConnectionPooler specSchema := "" specUser := "" From 97f4de7cc04289284308ad94b3c1fbca721b5f99 Mon Sep 17 00:00:00 2001 From: annielzy <148128409+annielzy@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:08:34 -0700 Subject: [PATCH 11/70] Fix rolling update deadlock when pods are stuck in non-running state (#3051) * add fix to recreate non running pods in syncStatefulsets * remove TestSyncStatefulSetNonRunningPodsDoNotBlockRecreatio * revert pod_test * pod without status --------- Co-authored-by: Felix Kunde Co-authored-by: Ida Novindasari --- pkg/cluster/pod.go | 36 ++++- pkg/cluster/pod_test.go | 300 ++++++++++++++++++++++++++++++++++++++++ pkg/cluster/sync.go | 16 ++- 3 files changed, 349 insertions(+), 3 deletions(-) diff --git a/pkg/cluster/pod.go b/pkg/cluster/pod.go index 959bacb54..6658ba414 100644 --- a/pkg/cluster/pod.go +++ b/pkg/cluster/pod.go @@ -376,6 +376,36 @@ func (c *Cluster) getPatroniMemberData(pod *v1.Pod) (patroni.MemberData, error) return memberData, nil } +// podIsNotRunning returns true if a pod is known to be in a non-running state, +// e.g. stuck in CreateContainerConfigError, CrashLoopBackOff, ImagePullBackOff, etc. +// Pods with no status information are not considered non-running, as they may +// simply not have reported status yet. +func podIsNotRunning(pod *v1.Pod) bool { + if pod.Status.Phase == "" { + // No status reported yet — don't treat as non-running + return false + } + if pod.Status.Phase != v1.PodRunning { + return true + } + for _, cs := range pod.Status.ContainerStatuses { + if cs.State.Waiting != nil || cs.State.Terminated != nil { + return true + } + } + return false +} + +// allPodsRunning returns true only if every pod in the list is in a healthy running state. +func (c *Cluster) allPodsRunning(pods []v1.Pod) bool { + for i := range pods { + if podIsNotRunning(&pods[i]) { + return false + } + } + return true +} + func (c *Cluster) recreatePod(podName spec.NamespacedName) (*v1.Pod, error) { stopCh := make(chan struct{}) ch := c.registerPodSubscriber(podName) @@ -444,7 +474,8 @@ func (c *Cluster) recreatePods(pods []v1.Pod, switchoverCandidates []spec.Namesp // switchover if // 1. we have not observed a new master pod when re-creating former replicas // 2. we know possible switchover targets even when no replicas were recreated - if newMasterPod == nil && len(replicas) > 0 { + // 3. the master pod is actually running (can't switchover a dead master) + if newMasterPod == nil && len(replicas) > 0 && !podIsNotRunning(masterPod) { masterCandidate, err := c.getSwitchoverCandidate(masterPod) if err != nil { // do not recreate master now so it will keep the update flag and switchover will be retried on next sync @@ -455,6 +486,9 @@ func (c *Cluster) recreatePods(pods []v1.Pod, switchoverCandidates []spec.Namesp } } else if newMasterPod == nil && len(replicas) == 0 { c.logger.Warningf("cannot perform switch over before re-creating the pod: no replicas") + } else if podIsNotRunning(masterPod) { + c.logger.Warningf("master pod %q is not running, skipping switchover and recreating directly", + util.NameFromMeta(masterPod.ObjectMeta)) } c.logger.Infof("recreating old master pod %q", util.NameFromMeta(masterPod.ObjectMeta)) diff --git a/pkg/cluster/pod_test.go b/pkg/cluster/pod_test.go index 6816b4d7a..6ab3f9207 100644 --- a/pkg/cluster/pod_test.go +++ b/pkg/cluster/pod_test.go @@ -15,6 +15,7 @@ import ( "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" ) func TestGetSwitchoverCandidate(t *testing.T) { @@ -112,3 +113,302 @@ func TestGetSwitchoverCandidate(t *testing.T) { } } } + +func TestPodIsNotRunning(t *testing.T) { + tests := []struct { + subtest string + pod v1.Pod + expected bool + }{ + { + subtest: "pod with no status reported yet", + pod: v1.Pod{ + Status: v1.PodStatus{}, + }, + expected: false, + }, + { + subtest: "pod running with all containers ready", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + expected: false, + }, + { + subtest: "pod in pending phase", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + expected: true, + }, + { + subtest: "pod running but container in CreateContainerConfigError", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "CreateContainerConfigError", + Message: `secret "some-secret" not found`, + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + subtest: "pod running but container in CrashLoopBackOff", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "CrashLoopBackOff", + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + subtest: "pod running but container terminated", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 137, + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + subtest: "pod running with mixed container states - one healthy one broken", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "CreateContainerConfigError", + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + subtest: "pod in failed phase", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodFailed, + }, + }, + expected: true, + }, + { + subtest: "pod running with multiple healthy containers", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + { + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + expected: false, + }, + { + subtest: "pod running with ImagePullBackOff", + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "ImagePullBackOff", + }, + }, + }, + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.subtest, func(t *testing.T) { + result := podIsNotRunning(&tt.pod) + if result != tt.expected { + t.Errorf("podIsNotRunning() = %v, expected %v", result, tt.expected) + } + }) + } +} + +func TestAllPodsRunning(t *testing.T) { + client, _ := newFakeK8sSyncClient() + + var cluster = New( + Config{ + OpConfig: config.Config{ + Resources: config.Resources{ + ClusterLabels: map[string]string{"application": "spilo"}, + ClusterNameLabel: "cluster-name", + PodRoleLabel: "spilo-role", + }, + }, + }, client, acidv1.Postgresql{}, logger, eventRecorder) + + tests := []struct { + subtest string + pods []v1.Pod + expected bool + }{ + { + subtest: "all pods running", + pods: []v1.Pod{ + { + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + {State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}}, + }, + }, + }, + { + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + {State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}}, + }, + }, + }, + }, + expected: true, + }, + { + subtest: "one pod not running", + pods: []v1.Pod{ + { + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + {State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}}, + }, + }, + }, + { + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "CreateContainerConfigError", + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + subtest: "all pods not running", + pods: []v1.Pod{ + { + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + { + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "CrashLoopBackOff", + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + subtest: "empty pod list", + pods: []v1.Pod{}, + expected: true, + }, + { + subtest: "pods with no status reported yet", + pods: []v1.Pod{ + { + Status: v1.PodStatus{}, + }, + { + Status: v1.PodStatus{}, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.subtest, func(t *testing.T) { + result := cluster.allPodsRunning(tt.pods) + if result != tt.expected { + t.Errorf("allPodsRunning() = %v, expected %v", result, tt.expected) + } + }) + } +} diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 3fa9e9783..7c478477a 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -719,14 +719,26 @@ func (c *Cluster) syncStatefulSet() error { if configPatched, restartPrimaryFirst, restartWait, err = c.syncPatroniConfig(pods, c.Spec.Patroni, requiredPgParameters); err != nil { c.logger.Warningf("Patroni config updated? %v - errors during config sync: %v", configPatched, err) postponeReasons = append(postponeReasons, "errors during Patroni config sync") - isSafeToRecreatePods = false + // Only mark unsafe if all pods are running. If some pods are not running, + // Patroni API errors are expected and should not block pod recreation, + // which is the only way to fix non-running pods. + if c.allPodsRunning(pods) { + isSafeToRecreatePods = false + } else { + c.logger.Warningf("ignoring Patroni config sync errors because some pods are not running") + } } // restart Postgres where it is still pending if err = c.restartInstances(pods, restartWait, restartPrimaryFirst); err != nil { c.logger.Errorf("errors while restarting Postgres in pods via Patroni API: %v", err) postponeReasons = append(postponeReasons, "errors while restarting Postgres via Patroni API") - isSafeToRecreatePods = false + // Same logic: don't let unreachable non-running pods block recreation. + if c.allPodsRunning(pods) { + isSafeToRecreatePods = false + } else { + c.logger.Warningf("ignoring Patroni restart errors because some pods are not running") + } } // if we get here we also need to re-create the pods (either leftovers from the old From e1713705f4d9b50a6914c9e8841013c25f8de6bf Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Tue, 28 Apr 2026 13:34:36 +0200 Subject: [PATCH 12/70] build multi-arch pooler image (#3077) * build multi-arch pooler image * add pooler build step in delivery.yaml and bump pooler version * pull from docker hub not zalando registry * add pooler step to ghcr workflow * pass infra roles to auth file via pooler entrypoint * introduce extra pooler secret for mounting auth_file * use pbgouncer as image name and push to ghcr on next merge * build with latest pgbouncer * integrate new image in e2e process and update pooler image default * update pooler build dependencies * build pooler image for e2e test * more Makefile and e2e run script tweaking --------- Co-authored-by: Ida Novindasari --- .github/workflows/publish_ghcr_image.yaml | 15 +++ Makefile | 8 +- .../crds/operatorconfigurations.yaml | 2 +- charts/postgres-operator/values.yaml | 2 +- delivery.yaml | 27 ++++ docs/reference/operator_parameters.md | 2 +- e2e/exec_into_env.sh | 4 +- e2e/run.sh | 46 ++++--- e2e/tests/test_e2e.py | 11 +- go.sum | 6 - manifests/configmap.yaml | 2 +- manifests/minimal-fake-pooler-deployment.yaml | 2 +- manifests/operatorconfiguration.crd.yaml | 2 +- ...gresql-operator-default-configuration.yaml | 2 +- pkg/cluster/connection_pooler.go | 119 +++++++++++++++++- pkg/cluster/connection_pooler_test.go | 5 + pkg/cluster/k8sres_test.go | 1 + pkg/controller/operator_config.go | 2 +- pkg/util/config/config.go | 2 +- pooler/Dockerfile | 54 ++++++++ pooler/entrypoint.sh | 19 +++ pooler/pgbouncer.ini.tmpl | 70 +++++++++++ 22 files changed, 363 insertions(+), 40 deletions(-) create mode 100644 pooler/Dockerfile create mode 100755 pooler/entrypoint.sh create mode 100644 pooler/pgbouncer.ini.tmpl diff --git a/.github/workflows/publish_ghcr_image.yaml b/.github/workflows/publish_ghcr_image.yaml index 3cead3503..2425e39b3 100644 --- a/.github/workflows/publish_ghcr_image.yaml +++ b/.github/workflows/publish_ghcr_image.yaml @@ -34,6 +34,12 @@ jobs: OPERATOR_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${GITHUB_REF/refs\/tags\//}" echo "OPERATOR_IMAGE=$OPERATOR_IMAGE" >> $GITHUB_OUTPUT + - name: Define pooler image name + id: image_pooler + run: | + POOLER_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/pgbouncer:${GITHUB_REF/refs\/tags\//}" + echo "POOLER_IMAGE=$POOLER_IMAGE" >> $GITHUB_OUTPUT + - name: Define UI image name id: image_ui run: | @@ -69,6 +75,15 @@ jobs: tags: "${{ steps.image.outputs.OPERATOR_IMAGE }}" platforms: linux/amd64,linux/arm64 + - name: Build and push multiarch pooler image to ghcr + uses: docker/build-push-action@v3 + with: + context: pooler + push: true + build-args: BASE_IMAGE=alpine:3.22 + tags: "${{ steps.image_pooler.outputs.POOLER_IMAGE }}" + platforms: linux/amd64,linux/arm64 + - name: Build and push multiarch ui image to ghcr uses: docker/build-push-action@v3 with: diff --git a/Makefile b/Makefile index b96d71939..c1becbc99 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean local test linux macos mocks docker push e2e +.PHONY: clean local test linux macos mocks docker pooler push e2e BINARY ?= postgres-operator BUILD_FLAGS ?= -v @@ -49,6 +49,7 @@ endif PATH := $(GOPATH)/bin:$(PATH) SHELL := env PATH="$(PATH)" $(SHELL) IMAGE_TAG := $(IMAGE):$(TAG)$(CDP_TAG)$(DEBUG_FRESH)$(DEBUG_POSTFIX) +POOLER_TAG := $(IMAGE)/pgbouncer:$(TAG)$(CDP_TAG)$(DEBUG_FRESH)$(DEBUG_POSTFIX) default: local @@ -95,6 +96,9 @@ docker: $(GENERATED_CRDS) ${DOCKERDIR}/${DOCKERFILE} echo "git describe $(shell git describe --tags --always --dirty)" docker build --rm -t "$(IMAGE_TAG)" -f "${DOCKERDIR}/${DOCKERFILE}" --build-arg VERSION="${VERSION}" --build-arg BASE_IMAGE="${BASE_IMAGE}" . +pooler: + cd pooler; docker build --rm -t "$(POOLER_TAG)" --build-arg VERSION="${VERSION}" --build-arg BASE_IMAGE="${BASE_IMAGE}" . + indocker-race: docker run --rm -v "${GOPATH}":"${GOPATH}" -e GOPATH="${GOPATH}" -e RACE=1 -w ${PWD} golang:1.25.3 bash -c "make linux" @@ -113,5 +117,5 @@ test: mocks $(GENERATED) $(GENERATED_CRDS) codegen: $(GENERATED) -e2e: docker # build operator image to be tested +e2e: docker pooler # build operator and pooler images to be tested cd e2e; make e2etest diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index cb4b7a335..80ef38d25 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -672,7 +672,7 @@ spec: default: "pooler" connection_pooler_image: type: string - default: "registry.opensource.zalan.do/acid/pgbouncer:master-32" + default: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" connection_pooler_max_db_connections: type: integer default: 60 diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index dfec76b6b..a1f4fa94c 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -443,7 +443,7 @@ configConnectionPooler: # db user for pooler to use connection_pooler_user: "pooler" # docker image - connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-32" + connection_pooler_image: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" # max db connections the pooler should hold connection_pooler_max_db_connections: 60 # default pooling mode diff --git a/delivery.yaml b/delivery.yaml index 933e72733..ac1ed90c6 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -42,6 +42,33 @@ pipeline: -f docker/Dockerfile \ --push . + - id: build-pooler + env: + <<: *BUILD_ENV + type: script + vm_config: + type: linux + + commands: + - desc: Build image + cmd: | + cd pooler + if [ -z ${CDP_SOURCE_BRANCH} ]; then + IMAGE=${MULTI_ARCH_REGISTRY}/pgbouncer + else + IMAGE=${MULTI_ARCH_REGISTRY}/pgbouncer-test + fi + + docker buildx create --config /etc/cdp-buildkitd.toml --driver-opt network=host --bootstrap --use + docker buildx build --platform "linux/amd64,linux/arm64" \ + --build-arg BASE_IMAGE="${ALPINE_BASE_IMAGE}" \ + -t "${IMAGE}:${CDP_BUILD_VERSION}" \ + --push . + + if [ -z ${CDP_SOURCE_BRANCH} ]; then + cdp-promote-image ${IMAGE}:${CDP_BUILD_VERSION} + fi + - id: build-operator-ui env: <<: *BUILD_ENV diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 83f693acc..bd2ca7cf6 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -1075,7 +1075,7 @@ operator being able to provide some reasonable defaults. * **connection_pooler_image** Docker image to use for connection pooler deployment. - Default: "registry.opensource.zalan.do/acid/pgbouncer" + Default: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" * **connection_pooler_max_db_connections** How many connections the pooler can max hold. This value is divided among the diff --git a/e2e/exec_into_env.sh b/e2e/exec_into_env.sh index a46efecbd..4d017bc35 100755 --- a/e2e/exec_into_env.sh +++ b/e2e/exec_into_env.sh @@ -3,6 +3,7 @@ export cluster_name="postgres-operator-e2e-tests" export kubeconfig_path="/tmp/kind-config-${cluster_name}" export operator_image="ghcr.io/zalando/postgres-operator:latest" +export pooler_image="ghcr.io/zalando/postgres-operator/pgbouncer:latest" export e2e_test_runner_image="ghcr.io/zalando/postgres-operator-e2e-tests-runner:latest" docker run -it --entrypoint /bin/bash --network=host -e "TERM=xterm-256color" \ @@ -11,4 +12,5 @@ docker run -it --entrypoint /bin/bash --network=host -e "TERM=xterm-256color" \ --mount type=bind,source="$(readlink -f tests)",target=/tests \ --mount type=bind,source="$(readlink -f exec.sh)",target=/exec.sh \ --mount type=bind,source="$(readlink -f scripts)",target=/scripts \ - -e OPERATOR_IMAGE="${operator_image}" "${e2e_test_runner_image}" + -e OPERATOR_IMAGE="${operator_image}" -e POOLER_IMAGE="${pooler_image}" \ + "${e2e_test_runner_image}" diff --git a/e2e/run.sh b/e2e/run.sh index f74158240..c5daf81f6 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -26,17 +26,33 @@ echo "Kubeconfig path: ${kubeconfig_path}" function pull_images(){ operator_tag=$(git describe --tags --always --dirty) - image_name="ghcr.io/zalando/postgres-operator:${operator_tag}" - if [[ -z $(docker images -q "${image_name}") ]] - then - if ! docker pull "${image_name}" - then - echo "Failed to pull operator image: ${image_name}" - exit 1 + components=("postgres-operator" "pooler") + image_urls=("ghcr.io/zalando/postgres-operator:${operator_tag}" "ghcr.io/zalando/postgres-operator/pgbouncer:${operator_tag}") + + for i in "${!components[@]}"; do + component="${components[$i]}" + image="${image_urls[$i]}" + + if [[ -z $(docker images -q "$image") ]]; then + echo "Pulling $component image: $image" + if ! docker pull "$image"; then + echo "Failed to pull $component image: $image" + exit 1 + fi + else + echo "$component image already exists: $image" fi - fi - operator_image="${image_name}" - echo "Using operator image: ${operator_image}" + + # Set variables for later use + if [[ "$component" == "postgres-operator" ]]; then + operator_image="$image" + elif [[ "$component" == "pooler" ]]; then + pooler_image="$image" + fi + done + + echo "Using operator image: $operator_image" + echo "Using pooler image: $pooler_image" } function start_kind(){ @@ -55,10 +71,11 @@ function start_kind(){ kind load docker-image "${spilo_image}" --name ${cluster_name} } -function load_operator_image() { - echo "Loading operator image" +function load_operator_images() { + echo "Loading operator images" export KUBECONFIG="${kubeconfig_path}" kind load docker-image "${operator_image}" --name ${cluster_name} + kind load docker-image "${pooler_image}" --name ${cluster_name} } function set_kind_api_server_ip(){ @@ -85,7 +102,8 @@ function run_tests(){ --mount type=bind,source="$(readlink -f tests)",target=/tests \ --mount type=bind,source="$(readlink -f exec.sh)",target=/exec.sh \ --mount type=bind,source="$(readlink -f scripts)",target=/scripts \ - -e OPERATOR_IMAGE="${operator_image}" "${e2e_test_runner_image}" ${E2E_TEST_CASE-} $@ + -e OPERATOR_IMAGE="${operator_image}" -e POOLER_IMAGE="${pooler_image}" \ + "${e2e_test_runner_image}" ${E2E_TEST_CASE-} $@ } function cleanup(){ @@ -100,7 +118,7 @@ function main(){ [[ -z ${NOCLEANUP-} ]] && trap "cleanup" QUIT TERM EXIT pull_images [[ ! -f ${kubeconfig_path} ]] && start_kind - load_operator_image + load_operator_images set_kind_api_server_ip generate_certificate diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 8cadb98a7..159c3cc79 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -116,6 +116,7 @@ def setUpClass(cls): configmap["data"]["workers"] = "1" configmap["data"]["docker_image"] = SPILO_CURRENT configmap["data"]["major_version_upgrade_mode"] = "full" + configmap["data"]["connection_pooler_image"] = os.environ['POOLER_IMAGE'] with open("manifests/configmap.yaml", 'w') as f: yaml.dump(configmap, f, Dumper=yaml.Dumper) @@ -698,7 +699,7 @@ def test_enable_disable_connection_pooler(self): self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label), 2, "No pooler pods found") self.eventuallyEqual(lambda: k8s.count_running_pods(replica_pooler_label), 2, "No pooler replica pods found") self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label), 2, "No pooler service found") - self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), 1, "Pooler secret not created") + self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), 3, "Not all pooler secrets found") # TLS still enabled so check existing env variables and volume mounts self.eventuallyEqual(lambda: k8s.count_pods_with_env_variable("CONNECTION_POOLER_CLIENT_TLS_CRT", pooler_label), 4, "TLS env variable CONNECTION_POOLER_CLIENT_TLS_CRT missing in pooler pods") @@ -756,7 +757,7 @@ def test_enable_disable_connection_pooler(self): self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label), 1, "No pooler service found") self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), - 1, "Secret not created") + 2, "Not all pooler secrets created") # Turn off only replica connection pooler k8s.api.custom_objects_api.patch_namespaced_custom_object( @@ -784,7 +785,7 @@ def test_enable_disable_connection_pooler(self): 'ClusterIP', "Expected LoadBalancer service type for master, found {}") self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), - 1, "Secret not created") + 2, "Not all pooler secrets created") # scale up connection pooler deployment k8s.api.custom_objects_api.patch_namespaced_custom_object( @@ -819,8 +820,8 @@ def test_enable_disable_connection_pooler(self): 0, "Pooler pods not scaled down") self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label), 0, "Pooler service not removed") - self.eventuallyEqual(lambda: k8s.count_secrets_with_label('application=spilo,cluster-name=acid-minimal-cluster'), - 4, "Secrets not deleted") + self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), + 0, "Not all pooler secrets deleted") # Verify that all the databases have pooler schema installed. # Do this via psql, since otherwise we need to deal with diff --git a/go.sum b/go.sum index 5b70c6899..a1fa39389 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -113,8 +111,6 @@ github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= @@ -126,7 +122,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -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= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -170,7 +165,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 571a4171b..1096e0265 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -17,7 +17,7 @@ data: connection_pooler_default_cpu_request: "500m" connection_pooler_default_memory_limit: 100Mi connection_pooler_default_memory_request: 100Mi - connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-32" + connection_pooler_image: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" connection_pooler_max_db_connections: "60" connection_pooler_mode: "transaction" connection_pooler_number_of_instances: "2" diff --git a/manifests/minimal-fake-pooler-deployment.yaml b/manifests/minimal-fake-pooler-deployment.yaml index 59a32ad0b..13f2cc68f 100644 --- a/manifests/minimal-fake-pooler-deployment.yaml +++ b/manifests/minimal-fake-pooler-deployment.yaml @@ -23,7 +23,7 @@ spec: serviceAccountName: postgres-operator containers: - name: postgres-operator - image: registry.opensource.zalan.do/acid/pgbouncer:master-32 + image: ghcr.io/zalando/postgres-operator/pgbouncer:latest imagePullPolicy: IfNotPresent resources: requests: diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 3be545b65..b5044b467 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -670,7 +670,7 @@ spec: default: "pooler" connection_pooler_image: type: string - default: "registry.opensource.zalan.do/acid/pgbouncer:master-32" + default: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" connection_pooler_max_db_connections: type: integer default: 60 diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 1c6a0e34a..13dfd6977 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -218,7 +218,7 @@ configuration: connection_pooler_default_cpu_request: "500m" connection_pooler_default_memory_limit: 100Mi connection_pooler_default_memory_request: 100Mi - connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-32" + connection_pooler_image: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" # connection_pooler_max_db_connections: 60 connection_pooler_mode: "transaction" connection_pooler_number_of_instances: 2 diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 336ffd4d9..97f7a3076 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -31,6 +31,7 @@ var poolerRunAsGroup = int64(101) // ConnectionPoolerObjects K8s objects that are belong to connection pooler type ConnectionPoolerObjects struct { + AuthSecret *v1.Secret Deployment *appsv1.Deployment Service *v1.Service Name string @@ -167,6 +168,38 @@ func (c *Cluster) createConnectionPooler(LookupFunction InstallFunction) (SyncRe return reason, nil } +func (c *Cluster) generateUserlist() string { + var sb strings.Builder + + poolerAdminUser := c.systemUsers[constants.ConnectionPoolerUserKeyName] + fmt.Fprintf(&sb, "\"%s\" \"%s\"\n", poolerAdminUser.Name, poolerAdminUser.Password) + + for roleName, infraRole := range c.InfrastructureRoles { + if infraRole.Password != "" { + fmt.Fprintf(&sb, "\"%s\" \"%s\"\n", roleName, infraRole.Password) + } + } + + return sb.String() +} + +func (c *Cluster) generateConnectionPoolerAuthSecret(connectionPooler *ConnectionPoolerObjects) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Labels: c.connectionPoolerLabels(connectionPooler.Role, true).MatchLabels, + Name: fmt.Sprintf("%s-userlist", connectionPooler.Name), + Namespace: connectionPooler.Namespace, + Annotations: c.annotationsSet(nil), + OwnerReferences: c.ownerReferences(), + }, + Type: v1.SecretTypeOpaque, + // Secret data must be bytes. Kubernetes handles the encoding. + StringData: map[string]string{ + "userlist.txt": c.generateUserlist(), + }, + } +} + // Generate pool size related environment variables. // // MAX_DB_CONN would specify the global maximum for connections to a target @@ -320,6 +353,18 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( } envVars = append(envVars, c.getConnectionPoolerEnvVars()...) + infraRolesList := make([]string, 0) + for infraRoleName := range c.InfrastructureRoles { + infraRolesList = append(infraRolesList, infraRoleName) + } + + if len(infraRolesList) > 0 { + envVars = append(envVars, v1.EnvVar{ + Name: "INFRASTRUCTURE_ROLES", + Value: strings.Join(infraRolesList, ","), + }) + } + poolerContainer := v1.Container{ Name: connectionPoolerContainer, Image: effectiveDockerImage, @@ -343,12 +388,29 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( }, } + var poolerVolumes []v1.Volume + var volumeMounts []v1.VolumeMount + + // mount secret volume with userlist.txt for pgBouncer to authenticate users + poolerVolumes = append(poolerVolumes, v1.Volume{ + Name: fmt.Sprintf("%s-userlist-volume", c.connectionPoolerName(role)), + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: fmt.Sprintf("%s-userlist", c.connectionPoolerName(role)), + }, + }, + }) + volumeMounts = append(volumeMounts, v1.VolumeMount{ + Name: fmt.Sprintf("%s-userlist-volume", c.connectionPoolerName(role)), + MountPath: "/etc/pgbouncer/userlist.txt", + SubPath: "userlist.txt", + ReadOnly: true, + }) + // If the cluster has custom TLS certificates configured, we do the following: // 1. Add environment variables to tell pgBouncer where to find the TLS certificates // 2. Reference the secret in a volume // 3. Mount the volume to the container at /tls - var poolerVolumes []v1.Volume - var volumeMounts []v1.VolumeMount if spec.TLS != nil && spec.TLS.SecretName != "" { getPoolerTLSEnv := func(k string) string { keyName := "" @@ -635,12 +697,31 @@ func (c *Cluster) deleteConnectionPooler(role PostgresRole) (err error) { c.logger.Infof("connection pooler service %s has been deleted for role %s", service.Name, role) } + // Repeat the same for the auth secret + authSecret := c.ConnectionPooler[role].AuthSecret + if authSecret == nil { + c.logger.Debug("no connection pooler auth secret to delete") + } else { + err := c.KubeClient. + Secrets(c.Namespace). + Delete(context.TODO(), authSecret.Name, metav1.DeleteOptions{}) + + if k8sutil.ResourceNotFound(err) { + c.logger.Debugf("connection pooler auth secret %s for role %s has already been deleted", authSecret.Name, role) + } else if err != nil { + return fmt.Errorf("could not delete connection pooler auth secret: %v", err) + } + + c.logger.Infof("connection pooler auth secret %s has been deleted for role %s", authSecret.Name, role) + } + + c.ConnectionPooler[role].AuthSecret = nil c.ConnectionPooler[role].Deployment = nil c.ConnectionPooler[role].Service = nil return nil } -// delete connection pooler +// delete connection pooler secret func (c *Cluster) deleteConnectionPoolerSecret() (err error) { // Repeat the same for the secret object secretName := c.credentialSecretName(c.OpConfig.ConnectionPooler.User) @@ -656,6 +737,7 @@ func (c *Cluster) deleteConnectionPoolerSecret() (err error) { return fmt.Errorf("could not delete pooler secret: %v", err) } } + return nil } @@ -971,11 +1053,42 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql pods []v1.Pod service *v1.Service newService *v1.Service + authSecret *v1.Secret + newAuthSecret *v1.Secret err error ) updatedPodAnnotations := map[string]*string{} syncReason := make([]string, 0) + + // create extra secret for connection pooler authentication + newAuthSecret = c.generateConnectionPoolerAuthSecret(c.ConnectionPooler[role]) + if authSecret, err = c.KubeClient.Secrets(c.Namespace).Get(context.TODO(), fmt.Sprintf("%s-userlist", c.connectionPoolerName(role)), metav1.GetOptions{}); err == nil { + c.ConnectionPooler[role].AuthSecret = authSecret + // make sure existing annotations are preserved + newAuthSecret.Annotations = c.annotationsSet(authSecret.Annotations) + authSecret, err = c.KubeClient.Secrets(authSecret.Namespace).Update(context.TODO(), newAuthSecret, metav1.UpdateOptions{}) + if err != nil { + return NoSync, fmt.Errorf("could not update connection pooler auth secret: %v", err) + } + c.ConnectionPooler[role].AuthSecret = authSecret + } else if !k8sutil.ResourceNotFound(err) { + return NoSync, fmt.Errorf("could not get auth secret for connection pooler to sync: %v", err) + } + + if k8sutil.ResourceNotFound(err) { + c.logger.Warningf("auth secret %s for connection pooler is not found, create it", fmt.Sprintf("%s-userlist", c.connectionPoolerName(role))) + authSecret, err = c.KubeClient. + Secrets(newAuthSecret.Namespace). + Create(context.TODO(), newAuthSecret, metav1.CreateOptions{}) + + if err != nil { + return NoSync, err + } + c.ConnectionPooler[role].AuthSecret = authSecret + } + + // next the pooler deployment deployment, err = c.KubeClient. Deployments(c.Namespace). Get(context.TODO(), c.connectionPoolerName(role), metav1.GetOptions{}) diff --git a/pkg/cluster/connection_pooler_test.go b/pkg/cluster/connection_pooler_test.go index 78d1c2527..23213520f 100644 --- a/pkg/cluster/connection_pooler_test.go +++ b/pkg/cluster/connection_pooler_test.go @@ -30,6 +30,7 @@ func newFakeK8sPoolerTestClient() (k8sutil.KubernetesClient, *fake.Clientset) { StatefulSetsGetter: clientSet.AppsV1(), DeploymentsGetter: clientSet.AppsV1(), ServicesGetter: clientSet.CoreV1(), + SecretsGetter: clientSet.CoreV1(), }, clientSet } @@ -803,6 +804,7 @@ func TestConnectionPoolerDeploymentSpec(t *testing.T) { } cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{ Master: { + AuthSecret: nil, Deployment: nil, Service: nil, LookupFunction: true, @@ -1019,6 +1021,7 @@ func TestPoolerTLS(t *testing.T) { // create pooler resources cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{} cluster.ConnectionPooler[Master] = &ConnectionPoolerObjects{ + AuthSecret: nil, Deployment: nil, Service: nil, Name: cluster.connectionPoolerName(Master), @@ -1089,12 +1092,14 @@ func TestConnectionPoolerServiceSpec(t *testing.T) { } cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{ Master: { + AuthSecret: nil, Deployment: nil, Service: nil, LookupFunction: false, Role: Master, }, Replica: { + AuthSecret: nil, Deployment: nil, Service: nil, LookupFunction: false, diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 04f6476a6..62481c7e3 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -2967,6 +2967,7 @@ func newLBFakeClient() (k8sutil.KubernetesClient, *fake.Clientset) { DeploymentsGetter: clientSet.AppsV1(), PodsGetter: clientSet.CoreV1(), ServicesGetter: clientSet.CoreV1(), + SecretsGetter: clientSet.CoreV1(), }, clientSet } diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 0a458618b..4df8a8bd2 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -275,7 +275,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.ConnectionPooler.Image = util.Coalesce( fromCRD.ConnectionPooler.Image, - "registry.opensource.zalan.do/acid/pgbouncer") + "ghcr.io/zalando/postgres-operator/pgbouncer:latest") result.ConnectionPooler.Mode = util.Coalesce( fromCRD.ConnectionPooler.Mode, diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 914d7a180..796594a89 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -155,7 +155,7 @@ type ConnectionPooler struct { NumberOfInstances *int32 `name:"connection_pooler_number_of_instances" default:"2"` Schema string `name:"connection_pooler_schema" default:"pooler"` User string `name:"connection_pooler_user" default:"pooler"` - Image string `name:"connection_pooler_image" default:"registry.opensource.zalan.do/acid/pgbouncer"` + Image string `name:"connection_pooler_image" default:"ghcr.io/zalando/postgres-operator/pgbouncer:latest"` Mode string `name:"connection_pooler_mode" default:"transaction"` MaxDBConnections *int32 `name:"connection_pooler_max_db_connections" default:"60"` ConnectionPoolerDefaultCPURequest string `name:"connection_pooler_default_cpu_request"` diff --git a/pooler/Dockerfile b/pooler/Dockerfile new file mode 100644 index 000000000..e7836e0f2 --- /dev/null +++ b/pooler/Dockerfile @@ -0,0 +1,54 @@ +ARG BASE_IMAGE=alpine:3.22 +FROM ${BASE_IMAGE} AS build_stage + +RUN apk add -U --no-cache \ + autoconf \ + automake \ + curl \ + gcc \ + libc-dev \ + libevent \ + libevent-dev \ + libtool \ + make \ + openssl-dev \ + pkgconfig \ + git + +WORKDIR /src + +RUN git clone --single-branch --depth 1 https://github.com/pgbouncer/pgbouncer.git . && \ + git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) + +RUN git submodule init && git submodule update + +RUN ./autogen.sh && \ + ./configure --prefix=/pgbouncer --with-libevent=/usr/lib && \ + sed -i '/dist_man_MANS/d' Makefile && \ + make && \ + make install + +FROM ${BASE_IMAGE} + +RUN apk -U upgrade --no-cache \ + && apk --no-cache add bash c-ares ca-certificates gettext libevent openssl postgresql-client + +RUN addgroup -g 101 -S pgbouncer && \ + adduser -u 100 -S pgbouncer -G pgbouncer && \ + mkdir -p /etc/pgbouncer /var/log/pgbouncer /var/run/pgbouncer /etc/ssl/certs + +COPY --from=build_stage /pgbouncer/bin/pgbouncer /bin/pgbouncer +COPY pgbouncer.ini.tmpl /etc/pgbouncer/ +COPY entrypoint.sh /entrypoint.sh + +RUN chown -R pgbouncer:pgbouncer \ + /var/log/pgbouncer \ + /var/run/pgbouncer \ + /etc/pgbouncer \ + /etc/ssl/certs \ + && chmod +x /entrypoint.sh + +USER pgbouncer:pgbouncer +WORKDIR /etc/pgbouncer + +ENTRYPOINT ["/bin/sh", "/entrypoint.sh"] diff --git a/pooler/entrypoint.sh b/pooler/entrypoint.sh new file mode 100755 index 000000000..326d63fbe --- /dev/null +++ b/pooler/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -ex + +if [ -z "${CONNECTION_POOLER_CLIENT_TLS_CRT}" ]; then + openssl req -nodes -new -x509 -subj /CN=spilo.dummy.org \ + -keyout /etc/ssl/certs/pgbouncer.key \ + -out /etc/ssl/certs/pgbouncer.crt +else + ln -s ${CONNECTION_POOLER_CLIENT_TLS_CRT} /etc/ssl/certs/pgbouncer.crt + ln -s ${CONNECTION_POOLER_CLIENT_TLS_KEY} /etc/ssl/certs/pgbouncer.key + if [ ! -z "${CONNECTION_POOLER_CLIENT_CA_FILE}" ]; then + ln -s ${CONNECTION_POOLER_CLIENT_CA_FILE} /etc/ssl/certs/ca.crt + fi +fi + +envsubst < /etc/pgbouncer/pgbouncer.ini.tmpl > /etc/pgbouncer/pgbouncer.ini + +exec /bin/pgbouncer /etc/pgbouncer/pgbouncer.ini diff --git a/pooler/pgbouncer.ini.tmpl b/pooler/pgbouncer.ini.tmpl new file mode 100644 index 000000000..c26cf1453 --- /dev/null +++ b/pooler/pgbouncer.ini.tmpl @@ -0,0 +1,70 @@ +# vim: set ft=dosini: + +[databases] +* = host=$PGHOST port=$PGPORT auth_user=$PGUSER +postgres = host=$PGHOST port=$PGPORT auth_user=$PGUSER + +[pgbouncer] +pool_mode = $CONNECTION_POOLER_MODE +listen_port = $CONNECTION_POOLER_PORT +listen_addr = * +admin_users = $PGUSER +stats_users = $INFRASTRUCTURE_ROLES +auth_dbname = postgres +auth_file = /etc/pgbouncer/userlist.txt +auth_query = SELECT * FROM $PGSCHEMA.user_lookup($1) +auth_type = md5 +logfile = /var/log/pgbouncer/pgbouncer.log +pidfile = /var/run/pgbouncer/pgbouncer.pid + +server_tls_sslmode = require +server_tls_ca_file = /etc/ssl/certs/pgbouncer.crt +server_tls_protocols = secure +client_tls_sslmode = require +client_tls_key_file = /etc/ssl/certs/pgbouncer.key +client_tls_cert_file = /etc/ssl/certs/pgbouncer.crt + +log_connections = 0 +log_disconnections = 0 + +# Number of prepared statements to cache on a server connection (zero value +# disables support of prepared statements). +max_prepared_statements = 200 + +# How many server connections to allow per user/database pair. +default_pool_size = $CONNECTION_POOLER_DEFAULT_SIZE + +# Add more server connections to pool if below this number. Improves behavior +# when usual load comes suddenly back after period of total inactivity. +# +# NOTE: This value is per pool, i.e. a pair of (db, user), not a global one. +# Which means on the higher level it has to be calculated from the max allowed +# database connections and number of databases and users. If not taken into +# account, then for too many users or databases PgBouncer will go crazy +# opening/evicting connections. For now disable it. +# +# min_pool_size = $CONNECTION_POOLER_MIN_SIZE + +# How many additional connections to allow to a pool +reserve_pool_size = $CONNECTION_POOLER_RESERVE_SIZE + +# Maximum number of client connections allowed. +max_client_conn = $CONNECTION_POOLER_MAX_CLIENT_CONN + +# Do not allow more than this many connections per database (regardless of +# pool, i.e. user) +max_db_connections = $CONNECTION_POOLER_MAX_DB_CONN + +# If a client has been in "idle in transaction" state longer, it will be +# disconnected. [seconds] +idle_transaction_timeout = 600 + +# If login failed, because of failure from connect() or authentication that +# pooler waits this much before retrying to connect. Default is 15. [seconds] +server_login_retry = 5 + +# To ignore extra parameter in startup packet. By default only 'database' and +# 'user' are allowed, all others raise error. This is needed to tolerate +# overenthusiastic JDBC wanting to unconditionally set 'extra_float_digits=2' +# in startup packet. +ignore_startup_parameters = extra_float_digits,options From 3ca188487623be314a44d6804dba931fefaf446d Mon Sep 17 00:00:00 2001 From: Mikkel Oscar Lyderik Larsen Date: Fri, 8 May 2026 09:16:10 +0200 Subject: [PATCH 13/70] Remove references to registry.opensource.zalan.do (#3092) Signed-off-by: Mikkel Oscar Lyderik Larsen --- delivery.yaml | 2 ++ logical-backup/Dockerfile | 2 +- pkg/cluster/cluster_test.go | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/delivery.yaml b/delivery.yaml index ac1ed90c6..d81f4ee5e 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -4,6 +4,7 @@ allow_concurrent_steps: true build_env: &BUILD_ENV PYTHON_BASE_IMAGE: container-registry.zalando.net/library/python-3.11-slim ALPINE_BASE_IMAGE: container-registry.zalando.net/library/alpine-3 + UBUNTU_BASE_IMAGE: container-registry.zalando.net/library/ubuntu-22.04 MULTI_ARCH_REGISTRY: container-registry-test.zalando.net/acid pipeline: @@ -126,6 +127,7 @@ pipeline: docker buildx create --config /etc/cdp-buildkitd.toml --driver-opt network=host --bootstrap --use docker buildx build --platform linux/amd64,linux/arm64 \ + --build-arg BASE_IMAGE="${UBUNTU_BASE_IMAGE}" \ -t ${IMAGE}:${CDP_BUILD_VERSION} \ --push . diff --git a/logical-backup/Dockerfile b/logical-backup/Dockerfile index 94b8f1a35..804eb65ad 100644 --- a/logical-backup/Dockerfile +++ b/logical-backup/Dockerfile @@ -1,4 +1,4 @@ -ARG BASE_IMAGE=registry.opensource.zalan.do/library/ubuntu-22.04:latest +ARG BASE_IMAGE=ubuntu:22.04 FROM ${BASE_IMAGE} LABEL maintainer="Team ACID @ Zalando " diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 8046943d4..56b9640ef 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -1581,8 +1581,8 @@ func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMoun 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" + img1 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.14.0" + img2 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" clientSet := fake.NewSimpleClientset() acidClientSet := fakeacidv1.NewSimpleClientset() From 618ac156e6ef9605a48172f8559a058a2d08bed1 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Fri, 8 May 2026 17:25:59 +0200 Subject: [PATCH 14/70] Volume mount length of pooler users (#3093) * shorten pooler secret mount * update postgres CRD in helm chart --- .../postgres-operator/crds/postgresqls.yaml | 4180 +++++++++++++++-- pkg/cluster/connection_pooler.go | 12 +- 2 files changed, 3853 insertions(+), 339 deletions(-) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index c801346e4..9b65b7663 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -7,191 +7,276 @@ metadata: spec: group: acid.zalan.do names: + categories: + - all kind: postgresql listKind: postgresqlList plural: postgresqls - singular: postgresql shortNames: - pg - categories: - - all + singular: postgresql scope: Namespaced versions: - - name: v1 - served: true - storage: true - subresources: - status: {} - additionalPrinterColumns: - - name: Team - type: string - description: Team responsible for Postgres cluster + - additionalPrinterColumns: + - description: Team responsible for Postgres cluster jsonPath: .spec.teamId - - name: Version + name: Team type: string - description: PostgreSQL version + - description: PostgreSQL version jsonPath: .spec.postgresql.version - - name: Pods - type: integer - description: Number of Pods per Postgres cluster - jsonPath: .spec.numberOfInstances - - name: Volume + name: Version type: string - description: Size of the bound volume + - description: Number of Pods per Postgres cluster + jsonPath: .spec.numberOfInstances + name: Pods + type: integer + - description: Size of the bound volume jsonPath: .spec.volume.size - - name: CPU-Request + name: Volume type: string - description: Requested CPU for Postgres containers + - description: Requested CPU for Postgres containers jsonPath: .spec.resources.requests.cpu - - name: Memory-Request + name: CPU-Request type: string - description: Requested memory for Postgres containers + - description: Requested memory for Postgres containers jsonPath: .spec.resources.requests.memory - - name: Age - type: date - jsonPath: .metadata.creationTimestamp - - name: Status + name: Memory-Request type: string - description: Current sync status of postgresql resource + - description: Age of the PostgreSQL cluster + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Current sync status of postgresql resource jsonPath: .status.PostgresClusterStatus + name: Status + type: string + name: v1 schema: openAPIV3Schema: - type: object - required: - - kind - - apiVersion - - spec + description: Postgresql defines PostgreSQL Custom Resource Definition Object. properties: - kind: - type: string - enum: - - postgresql apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - enum: - - acid.zalan.do/v1 - spec: + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: type: object - required: - - numberOfInstances - - teamId - - postgresql - - volume + spec: + description: PostgresSpec defines the specification for the PostgreSQL + TPR. properties: additionalVolumes: - type: array items: - type: object - required: - - name - - mountPath - - volumeSource + description: AdditionalVolume specs additional optional volumes + for statefulset properties: isSubPathExpr: type: boolean - name: - type: string mountPath: type: string + name: + type: string subPath: type: string targetContainers: - type: array - nullable: true items: type: string + nullable: true + type: array volumeSource: type: object x-kubernetes-preserve-unknown-fields: true - allowedSourceRanges: + required: + - mountPath + - name + - volumeSource + type: object type: array - nullable: true + allowedSourceRanges: + description: load balancers' source ranges are the same for master + and replica services items: + pattern: ^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$ type: string - pattern: '^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$' + nullable: true + type: array clone: - type: object - required: - - cluster + description: CloneDescription describes which cluster the new should + clone and up to which point in time properties: cluster: type: string - s3_endpoint: - type: string s3_access_key_id: type: string - s3_secret_access_key: + s3_endpoint: type: string s3_force_path_style: type: boolean + s3_secret_access_key: + type: string s3_wal_path: type: string timestamp: + description: |- + The regexp matches the date-time format (RFC 3339 Section 5.6) that specifies a timezone as an offset relative to UTC + Example: 1996-12-19T16:39:57-08:00 + Note: this field requires a timezone + pattern: ^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([+-]([01][0-9]|2[0-3]):[0-5][0-9]))$ type: string - pattern: '^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([+-]([01][0-9]|2[0-3]):[0-5][0-9]))$' - # The regexp matches the date-time format (RFC 3339 Section 5.6) that specifies a timezone as an offset relative to UTC - # Example: 1996-12-19T16:39:57-08:00 - # Note: this field requires a timezone uid: format: uuid type: string - connectionPooler: + required: + - cluster type: object + connectionPooler: + description: |- + ConnectionPooler Options for connection pooler + + pgbouncer-large (with higher resources) or odyssey-small (with smaller + resources) + Type string `json:"type,omitempty"` + + makes sense to expose. E.g. pool size (min/max boundaries), max client + connections etc. properties: dockerImage: type: string maxDBConnections: + format: int32 type: integer mode: - type: string enum: - - "session" - - "transaction" + - session + - transaction + type: string numberOfInstances: - type: integer + format: int32 minimum: 1 + type: integer resources: - type: object + description: Resources describes requests and limits for the cluster + resouces. properties: limits: - type: object + description: ResourceDescription describes CPU and memory + resources defined for a cluster. properties: cpu: + description: |- + Decimal natural followed by m, or decimal natural followed by + dot followed by up to three decimal digits. + + This is because the Kubernetes CPU resource has millis as the + maximum precision. The actual values are checked in code + because the regular expression would be huge and horrible and + not very helpful in validation error messages; this one checks + only the format of the given number. + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + + Note: the value specified here must not be zero or be lower + than the corresponding request. + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + hugepages-1Gi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + hugepages-2Mi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' memory: + description: |- + You can express memory as a plain integer or as a fixed-point + integer using one of these suffixes: E, P, T, G, M, k. You can + also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + + Note: the value specified here must not be zero or be higher + than the corresponding limit. + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - requests: type: object + requests: + description: ResourceDescription describes CPU and memory + resources defined for a cluster. properties: cpu: + description: |- + Decimal natural followed by m, or decimal natural followed by + dot followed by up to three decimal digits. + + This is because the Kubernetes CPU resource has millis as the + maximum precision. The actual values are checked in code + because the regular expression would be huge and horrible and + not very helpful in validation error messages; this one checks + only the format of the given number. + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + + Note: the value specified here must not be zero or be lower + than the corresponding request. + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + hugepages-1Gi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + hugepages-2Mi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' memory: + description: |- + You can express memory as a plain integer or as a fixed-point + integer using one of these suffixes: E, P, T, G, M, k. You can + also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + + Note: the value specified here must not be zero or be higher + than the corresponding limit. + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + type: object + type: object schema: type: string user: type: string - databases: type: object + databases: additionalProperties: type: string - # Note: usernames specified here as database owners must be declared in the users key of the spec key. + description: |- + Note: usernames specified here as database owners must be declared + in the users key of the spec key. + type: object dockerImage: type: string enableConnectionPooler: type: boolean - enableReplicaConnectionPooler: - type: boolean enableLogicalBackup: type: boolean enableMasterLoadBalancer: + description: |- + vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest + in that case the var evaluates to nil and the value is taken from the operator config type: boolean enableMasterPoolerLoadBalancer: type: boolean + enableReplicaConnectionPooler: + type: boolean enableReplicaLoadBalancer: type: boolean enableReplicaPoolerLoadBalancer: @@ -199,302 +284,3645 @@ spec: enableShmVolume: type: boolean env: - type: array - nullable: true items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name type: object - x-kubernetes-preserve-unknown-fields: true - init_containers: type: array + init_containers: description: deprecated - nullable: true - items: - type: object - x-kubernetes-preserve-unknown-fields: true - initContainers: - type: array - nullable: true - items: - type: object - x-kubernetes-preserve-unknown-fields: true - logicalBackupRetention: - type: string - logicalBackupSchedule: - type: string - pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' - maintenanceWindows: - 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))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' - masterServiceAnnotations: - type: object - additionalProperties: - type: string - nodeAffinity: - type: object - properties: - preferredDuringSchedulingIgnoredDuringExecution: - type: array - items: - type: object - required: - - preference - - weight - properties: - preference: - type: object - properties: - matchExpressions: - type: array - items: - type: object - required: - - key - - operator + description: A single application container that you want to run + within a pod. + properties: + args: + description: |- + Arguments to the entrypoint. + The container image's CMD is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + description: |- + Entrypoint array. Not executed within a shell. + The container image's ENTRYPOINT is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + description: |- + List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: key: + description: The key to select. type: string - operator: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string - values: - type: array - items: - type: string - matchFields: - type: array - items: - type: object + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean required: - key - - operator + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: - key: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". type: string - operator: + fieldPath: + description: Path of the field to select in the + specified API version. type: string - values: - type: array - items: - type: string - weight: - type: integer - requiredDuringSchedulingIgnoredDuringExecution: - type: object - required: - - nodeSelectorTerms - properties: - nodeSelectorTerms: - type: array - items: - type: object - properties: - matchExpressions: - type: array - items: + required: + - fieldPath type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string required: - - key - - operator + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace properties: key: + description: The key of the secret to select from. Must + be a valid secret key. type: string - operator: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string - values: - type: array - items: - type: string - matchFields: - type: array - items: - type: object + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean required: - key - - operator + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + description: |- + List of sources to populate environment variables in the container. + The keys defined within a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is starting. When a key exists in multiple + sources, the value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will take precedence. + Cannot be updated. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each + key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + description: |- + Container image name. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + lifecycle: + description: |- + Actions that the management system should take in response to container lifecycle events. + Cannot be updated. + properties: + postStart: + description: |- + PostStart is called immediately after a container is created. If the handler fails, + the container is terminated and restarted according to its restart policy. + Other management of the container blocks until the hook completes. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: |- + PreStop is called immediately before a container is terminated due to an + API request or management event such as liveness/startup probe failure, + preemption, resource contention, etc. The handler is not called if the + container crashes or exits. The Pod's termination grace period countdown begins before the + PreStop hook is executed. Regardless of the outcome of the handler, the + container will eventually terminate within the Pod's termination grace + period (unless delayed by finalizers). Other management of the container blocks until the hook completes + or until the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: |- + Periodic probe of container liveness. + Container will be restarted if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes properties: - key: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. type: string - operator: + value: + description: The header field value type: string - values: - type: array - items: - type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + name: + description: |- + Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: |- + List of ports to expose from the container. Not specifying a port here + DOES NOT prevent that port from being exposed. Any port which is + listening on the default "0.0.0.0" address inside a container will be + accessible from the network. + Modifying this array with strategic merge patch may corrupt the data. + For more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: |- + Periodic probe of container service readiness. + Container will be removed from service endpoints if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + resizePolicy: + description: Resources resize policy for the container. + items: + description: ContainerResizePolicy represents resource resize + policy for the container. + properties: + resourceName: + description: |- + Name of the resource to which this resource resize policy applies. + Supported values: cpu, memory. + type: string + restartPolicy: + description: |- + Restart policy to apply when specified resource is resized. + If not specified, it defaults to NotRequired. + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + Compute Resources required by this container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restartPolicy: + description: |- + RestartPolicy defines the restart behavior of individual containers in a pod. + This field may only be set for init containers, and the only allowed value is "Always". + For non-init containers or when this field is not specified, + the restart behavior is defined by the Pod's restart policy and the container type. + Setting the RestartPolicy as "Always" for the init container will have the following effect: + this init container will be continually restarted on + exit until all regular containers have terminated. Once all regular + containers have completed, all init containers with restartPolicy "Always" + will be shut down. This lifecycle differs from normal init containers and + is often referred to as a "sidecar" container. Although this init + container still starts in the init container sequence, it does not wait + for the container to complete before proceeding to the next init + container. Instead, the next init container starts immediately after this + init container is started, or after any startupProbe has successfully + completed. + type: string + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: |- + StartupProbe indicates that the Pod has successfully initialized. + If specified, no other probes are executed until this completes successfully. + If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. + This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, + when it might take a long time to load data or warm a cache, than during steady-state operation. + This cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + stdin: + description: |- + Whether this container should allocate a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will always result in EOF. + Default is false. + type: boolean + stdinOnce: + description: |- + Whether the container runtime should close the stdin channel after it has been opened by + a single attach. When stdin is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the + first client attaches to stdin, and then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container is restarted. If this + flag is false, a container processes that reads from stdin will never receive an EOF. + Default is false + type: boolean + terminationMessagePath: + description: |- + Optional: Path at which the file to which the container's termination message + will be written is mounted into the container's filesystem. + Message written is intended to be brief final status, such as an assertion failure message. + Will be truncated by the node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. + Defaults to /dev/termination-log. + Cannot be updated. + type: string + terminationMessagePolicy: + description: |- + Indicate how the termination message should be populated. File will use the contents of + terminationMessagePath to populate the container status message on both success and failure. + FallbackToLogsOnError will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever is smaller. + Defaults to File. + Cannot be updated. + type: string + tty: + description: |- + Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + description: |- + Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + description: |- + Container's working directory. + If not specified, the container runtime's default will be used, which + might be configured in the container image. + Cannot be updated. + type: string + required: + - name + type: object + type: array + initContainers: + items: + description: A single application container that you want to run + within a pod. + properties: + args: + description: |- + Arguments to the entrypoint. + The container image's CMD is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + description: |- + Entrypoint array. Not executed within a shell. + The container image's ENTRYPOINT is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + description: |- + List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + description: |- + List of sources to populate environment variables in the container. + The keys defined within a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is starting. When a key exists in multiple + sources, the value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will take precedence. + Cannot be updated. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each + key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + description: |- + Container image name. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + lifecycle: + description: |- + Actions that the management system should take in response to container lifecycle events. + Cannot be updated. + properties: + postStart: + description: |- + PostStart is called immediately after a container is created. If the handler fails, + the container is terminated and restarted according to its restart policy. + Other management of the container blocks until the hook completes. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: |- + PreStop is called immediately before a container is terminated due to an + API request or management event such as liveness/startup probe failure, + preemption, resource contention, etc. The handler is not called if the + container crashes or exits. The Pod's termination grace period countdown begins before the + PreStop hook is executed. Regardless of the outcome of the handler, the + container will eventually terminate within the Pod's termination grace + period (unless delayed by finalizers). Other management of the container blocks until the hook completes + or until the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: |- + Periodic probe of container liveness. + Container will be restarted if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + name: + description: |- + Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: |- + List of ports to expose from the container. Not specifying a port here + DOES NOT prevent that port from being exposed. Any port which is + listening on the default "0.0.0.0" address inside a container will be + accessible from the network. + Modifying this array with strategic merge patch may corrupt the data. + For more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: |- + Periodic probe of container service readiness. + Container will be removed from service endpoints if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + resizePolicy: + description: Resources resize policy for the container. + items: + description: ContainerResizePolicy represents resource resize + policy for the container. + properties: + resourceName: + description: |- + Name of the resource to which this resource resize policy applies. + Supported values: cpu, memory. + type: string + restartPolicy: + description: |- + Restart policy to apply when specified resource is resized. + If not specified, it defaults to NotRequired. + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + Compute Resources required by this container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restartPolicy: + description: |- + RestartPolicy defines the restart behavior of individual containers in a pod. + This field may only be set for init containers, and the only allowed value is "Always". + For non-init containers or when this field is not specified, + the restart behavior is defined by the Pod's restart policy and the container type. + Setting the RestartPolicy as "Always" for the init container will have the following effect: + this init container will be continually restarted on + exit until all regular containers have terminated. Once all regular + containers have completed, all init containers with restartPolicy "Always" + will be shut down. This lifecycle differs from normal init containers and + is often referred to as a "sidecar" container. Although this init + container still starts in the init container sequence, it does not wait + for the container to complete before proceeding to the next init + container. Instead, the next init container starts immediately after this + init container is started, or after any startupProbe has successfully + completed. + type: string + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: |- + StartupProbe indicates that the Pod has successfully initialized. + If specified, no other probes are executed until this completes successfully. + If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. + This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, + when it might take a long time to load data or warm a cache, than during steady-state operation. + This cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + stdin: + description: |- + Whether this container should allocate a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will always result in EOF. + Default is false. + type: boolean + stdinOnce: + description: |- + Whether the container runtime should close the stdin channel after it has been opened by + a single attach. When stdin is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the + first client attaches to stdin, and then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container is restarted. If this + flag is false, a container processes that reads from stdin will never receive an EOF. + Default is false + type: boolean + terminationMessagePath: + description: |- + Optional: Path at which the file to which the container's termination message + will be written is mounted into the container's filesystem. + Message written is intended to be brief final status, such as an assertion failure message. + Will be truncated by the node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. + Defaults to /dev/termination-log. + Cannot be updated. + type: string + terminationMessagePolicy: + description: |- + Indicate how the termination message should be populated. File will use the contents of + terminationMessagePath to populate the container status message on both success and failure. + FallbackToLogsOnError will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever is smaller. + Defaults to File. + Cannot be updated. + type: string + tty: + description: |- + Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + description: |- + Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + description: |- + Container's working directory. + If not specified, the container runtime's default will be used, which + might be configured in the container image. + Cannot be updated. + type: string + required: + - name + type: object + type: array + logicalBackupRetention: + type: string + logicalBackupSchedule: + pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$ + type: string + maintenanceWindows: + items: + 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))\ *$' + type: string + type: array + masterServiceAnnotations: + additionalProperties: + type: string + description: MasterServiceAnnotations takes precedence over ServiceAnnotations + for master role if not empty + type: object + nodeAffinity: + description: Node affinity is a group of node affinity scheduling + rules. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the corresponding + weight. + properties: + matchExpressions: + description: A list of node selector requirements by + node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements by + node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements by + node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements by + node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object numberOfInstances: - type: integer + format: int32 minimum: 0 + type: integer patroni: - type: object + description: Patroni contains Patroni-specific configuration properties: failsafe_mode: type: boolean initdb: - type: object additionalProperties: type: string + type: object loop_wait: + format: int32 type: integer maximum_lag_on_failover: + format: int64 type: integer pg_hba: - type: array items: type: string + type: array retry_timeout: + format: int32 type: integer slots: - type: object additionalProperties: - type: object additionalProperties: type: string + type: object + type: object synchronous_mode: type: boolean synchronous_mode_strict: type: boolean synchronous_node_count: + format: int32 type: integer ttl: + format: int32 type: integer - podAnnotations: type: object - additionalProperties: - type: string pod_priority_class_name: - type: string description: deprecated + type: string + podAnnotations: + additionalProperties: + type: string + type: object podPriorityClassName: type: string postgresql: - type: object - required: - - version + description: PostgresqlParam describes PostgreSQL version and pairs + of configuration parameter name - values. properties: - version: - type: string - enum: - - "14" - - "15" - - "16" - - "17" - - "18" parameters: - type: object additionalProperties: type: string - preparedDatabases: + type: object + version: + enum: + - "14" + - "15" + - "16" + - "17" + - "18" + type: string + required: + - version type: object + preparedDatabases: additionalProperties: - type: object + description: PreparedDatabase describes elements to be bootstrapped properties: defaultUsers: type: boolean extensions: - type: object additionalProperties: type: string - schemas: type: object + schemas: additionalProperties: - type: object + description: PreparedSchema describes elements to be bootstrapped + per schema properties: - defaultUsers: - type: boolean defaultRoles: type: boolean + defaultUsers: + type: boolean + type: object + type: object secretNamespace: type: string + type: object + type: object replicaLoadBalancer: - type: boolean description: deprecated + type: boolean replicaServiceAnnotations: - type: object additionalProperties: type: string - resources: + description: ReplicaServiceAnnotations takes precedence over ServiceAnnotations + for replica role if not empty type: object + resources: + description: Resources describes requests and limits for the cluster + resouces. properties: limits: - type: object + description: ResourceDescription describes CPU and memory resources + defined for a cluster. properties: cpu: + description: |- + Decimal natural followed by m, or decimal natural followed by + dot followed by up to three decimal digits. + + This is because the Kubernetes CPU resource has millis as the + maximum precision. The actual values are checked in code + because the regular expression would be huge and horrible and + not very helpful in validation error messages; this one checks + only the format of the given number. + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + + Note: the value specified here must not be zero or be lower + than the corresponding request. + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ type: string - # Decimal natural followed by m, or decimal natural followed by - # dot followed by up to three decimal digits. - # - # This is because the Kubernetes CPU resource has millis as the - # maximum precision. The actual values are checked in code - # because the regular expression would be huge and horrible and - # not very helpful in validation error messages; this one checks - # only the format of the given number. - # - # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - # Note: the value specified here must not be zero or be lower - # than the corresponding request. - memory: + hugepages-1Gi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - # You can express memory as a plain integer or as a fixed-point - # integer using one of these suffixes: E, P, T, G, M, k. You can - # also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki - # - # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - # Note: the value specified here must not be zero or be higher - # than the corresponding limit. hugepages-2Mi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - hugepages-1Gi: + memory: + description: |- + You can express memory as a plain integer or as a fixed-point + integer using one of these suffixes: E, P, T, G, M, k. You can + also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + + Note: the value specified here must not be zero or be higher + than the corresponding limit. + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - requests: type: object + requests: + description: ResourceDescription describes CPU and memory resources + defined for a cluster. properties: cpu: + description: |- + Decimal natural followed by m, or decimal natural followed by + dot followed by up to three decimal digits. + + This is because the Kubernetes CPU resource has millis as the + maximum precision. The actual values are checked in code + because the regular expression would be huge and horrible and + not very helpful in validation error messages; this one checks + only the format of the given number. + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + + Note: the value specified here must not be zero or be lower + than the corresponding request. + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - memory: + hugepages-1Gi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' hugepages-2Mi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - hugepages-1Gi: + memory: + description: |- + You can express memory as a plain integer or as a fixed-point + integer using one of these suffixes: E, P, T, G, M, k. You can + also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + + Note: the value specified here must not be zero or be higher + than the corresponding limit. + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + type: object + type: object schedulerName: type: string serviceAnnotations: - type: object additionalProperties: type: string + type: object sidecars: - type: array - nullable: true items: + description: Sidecar defines a container to be run in the same pod + as the Postgres container. + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: Resources describes requests and limits for the + cluster resouces. + properties: + limits: + description: ResourceDescription describes CPU and memory + resources defined for a cluster. + properties: + cpu: + description: |- + Decimal natural followed by m, or decimal natural followed by + dot followed by up to three decimal digits. + + This is because the Kubernetes CPU resource has millis as the + maximum precision. The actual values are checked in code + because the regular expression would be huge and horrible and + not very helpful in validation error messages; this one checks + only the format of the given number. + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + + Note: the value specified here must not be zero or be lower + than the corresponding request. + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + hugepages-1Gi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + hugepages-2Mi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + memory: + description: |- + You can express memory as a plain integer or as a fixed-point + integer using one of these suffixes: E, P, T, G, M, k. You can + also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + + Note: the value specified here must not be zero or be higher + than the corresponding limit. + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + type: object + requests: + description: ResourceDescription describes CPU and memory + resources defined for a cluster. + properties: + cpu: + description: |- + Decimal natural followed by m, or decimal natural followed by + dot followed by up to three decimal digits. + + This is because the Kubernetes CPU resource has millis as the + maximum precision. The actual values are checked in code + because the regular expression would be huge and horrible and + not very helpful in validation error messages; this one checks + only the format of the given number. + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + + Note: the value specified here must not be zero or be lower + than the corresponding request. + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + hugepages-1Gi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + hugepages-2Mi: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + memory: + description: |- + You can express memory as a plain integer or as a fixed-point + integer using one of these suffixes: E, P, T, G, M, k. You can + also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + + https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + + Note: the value specified here must not be zero or be higher + than the corresponding limit. + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + type: object + type: object type: object - x-kubernetes-preserve-unknown-fields: true - spiloRunAsUser: + type: array + spiloFSGroup: + format: int64 type: integer spiloRunAsGroup: + format: int64 type: integer - spiloFSGroup: + spiloRunAsUser: + format: int64 type: integer standby: - type: object - properties: - s3_wal_path: - type: string - gs_wal_path: - type: string - standby_host: - type: string - standby_port: - type: string - standby_primary_slot_name: - type: string anyOf: - required: - s3_wal_path @@ -504,41 +3932,52 @@ spec: - standby_host not: required: - - s3_wal_path - - gs_wal_path + - s3_wal_path + - gs_wal_path + description: |- + StandbyDescription contains remote primary config and/or s3/gs wal path. + standby_host can be specified alone or together with either s3_wal_path OR gs_wal_path (mutually exclusive). + At least one field must be specified. s3_wal_path and gs_wal_path are mutually exclusive. + properties: + gs_wal_path: + type: string + s3_wal_path: + type: string + standby_host: + type: string + standby_port: + type: string + standby_primary_slot_name: + type: string + type: object streams: - type: array items: - type: object - required: - - applicationId - - database - - tables + description: Stream defines properties for creating FabricEventStream + resources properties: applicationId: type: string batchSize: + format: int32 type: integer cpu: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' database: type: string enableRecovery: type: boolean filter: - type: object additionalProperties: type: string + type: object memory: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' tables: - type: object additionalProperties: - type: object - required: - - eventType + description: StreamTable defines properties of outbox tables + for FabricEventStreams properties: eventType: type: string @@ -550,55 +3989,82 @@ spec: type: string recoveryEventType: type: string + required: + - eventType + type: object + type: object + required: + - applicationId + - database + - tables + type: object + type: array teamId: type: string tls: - type: object - required: - - secretName + description: TLSDescription specs TLS properties properties: - secretName: + caFile: + type: string + caSecretName: type: string certificateFile: type: string privateKeyFile: type: string - caFile: - type: string - caSecretName: + secretName: type: string + required: + - secretName + type: object tolerations: - type: array items: - type: object + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. type: string operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. type: string - enum: - - Equal - - Exists - value: - type: string - effect: - type: string - enum: - - NoExecute - - NoSchedule - - PreferNoSchedule tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array useLoadBalancer: + description: |- + deprecated load balancer settings maintained for backward compatibility + see "Load balancers" operator docs type: boolean - description: deprecated users: - type: object additionalProperties: - type: array - nullable: true + description: UserFlags defines flags (such as superuser, nologin) + that could be assigned to individual users items: - type: string enum: - bypassrls - BYPASSRLS @@ -628,68 +4094,116 @@ spec: - SUPERUSER - nosuperuser - NOSUPERUSER + type: string + type: array + type: object usersIgnoringSecretRotation: - type: array - nullable: true items: type: string - usersWithInPlaceSecretRotation: - type: array nullable: true + type: array + usersWithInPlaceSecretRotation: items: type: string - usersWithSecretRotation: - type: array nullable: true + type: array + usersWithSecretRotation: items: type: string + nullable: true + type: array volume: - type: object - required: - - size + description: Volume describes a single volume in the manifest. properties: - isSubPathExpr: - type: boolean iops: + format: int64 type: integer + isSubPathExpr: + type: boolean selector: - type: object + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. properties: matchExpressions: - type: array + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. items: - type: object - required: - - key - - operator + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: + description: key is the label key that the selector + applies to. type: string operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string - enum: - - DoesNotExist - - Exists - - In - - NotIn values: - type: array + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object - x-kubernetes-preserve-unknown-fields: true + type: object + x-kubernetes-map-type: atomic size: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - # Note: the value specified here must not be zero. storageClass: type: string subPath: type: string throughput: + format: int64 type: integer + type: + type: string + required: + - size + type: object + required: + - numberOfInstances + - postgresql + - teamId + - volume + type: object status: + description: PostgresStatus contains status of the PostgreSQL cluster + (running, creation failed etc.) + properties: + PostgresClusterStatus: + type: string + required: + - PostgresClusterStatus type: object - additionalProperties: - type: string + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 97f7a3076..9f071068c 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -187,7 +187,7 @@ func (c *Cluster) generateConnectionPoolerAuthSecret(connectionPooler *Connectio return &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Labels: c.connectionPoolerLabels(connectionPooler.Role, true).MatchLabels, - Name: fmt.Sprintf("%s-userlist", connectionPooler.Name), + Name: fmt.Sprintf("%s-u", connectionPooler.Name), Namespace: connectionPooler.Namespace, Annotations: c.annotationsSet(nil), OwnerReferences: c.ownerReferences(), @@ -393,15 +393,15 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( // mount secret volume with userlist.txt for pgBouncer to authenticate users poolerVolumes = append(poolerVolumes, v1.Volume{ - Name: fmt.Sprintf("%s-userlist-volume", c.connectionPoolerName(role)), + Name: fmt.Sprintf("%s-u", c.connectionPoolerName(role)), VolumeSource: v1.VolumeSource{ Secret: &v1.SecretVolumeSource{ - SecretName: fmt.Sprintf("%s-userlist", c.connectionPoolerName(role)), + SecretName: fmt.Sprintf("%s-u", c.connectionPoolerName(role)), }, }, }) volumeMounts = append(volumeMounts, v1.VolumeMount{ - Name: fmt.Sprintf("%s-userlist-volume", c.connectionPoolerName(role)), + Name: fmt.Sprintf("%s-u", c.connectionPoolerName(role)), MountPath: "/etc/pgbouncer/userlist.txt", SubPath: "userlist.txt", ReadOnly: true, @@ -1063,7 +1063,7 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql // create extra secret for connection pooler authentication newAuthSecret = c.generateConnectionPoolerAuthSecret(c.ConnectionPooler[role]) - if authSecret, err = c.KubeClient.Secrets(c.Namespace).Get(context.TODO(), fmt.Sprintf("%s-userlist", c.connectionPoolerName(role)), metav1.GetOptions{}); err == nil { + if authSecret, err = c.KubeClient.Secrets(c.Namespace).Get(context.TODO(), fmt.Sprintf("%s-u", c.connectionPoolerName(role)), metav1.GetOptions{}); err == nil { c.ConnectionPooler[role].AuthSecret = authSecret // make sure existing annotations are preserved newAuthSecret.Annotations = c.annotationsSet(authSecret.Annotations) @@ -1077,7 +1077,7 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql } if k8sutil.ResourceNotFound(err) { - c.logger.Warningf("auth secret %s for connection pooler is not found, create it", fmt.Sprintf("%s-userlist", c.connectionPoolerName(role))) + c.logger.Warningf("auth secret %s for connection pooler is not found, create it", fmt.Sprintf("%s-u", c.connectionPoolerName(role))) authSecret, err = c.KubeClient. Secrets(newAuthSecret.Namespace). Create(context.TODO(), newAuthSecret, metav1.CreateOptions{}) From 4d40270890a46aac133f45339576b39b797a8ae9 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Mon, 11 May 2026 02:16:37 -0700 Subject: [PATCH 15/70] fix: correct 'occured' typo in error messages (#3094) * fix: correct 'occured' typo in finalizer error message * fix: correct 'occured' typo in EBS volume error message --- pkg/cluster/cluster.go | 2 +- pkg/cluster/volumes.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 8e0b3c79f..488964b99 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -1293,7 +1293,7 @@ func (c *Cluster) Delete() error { // If we are done deleting our various resources we remove the finalizer to let K8S finally delete the Postgres CR if anyErrors { c.eventRecorder.Event(c.GetReference(), v1.EventTypeWarning, "Delete", "some resources could be successfully deleted yet") - return fmt.Errorf("some error(s) occured when deleting resources, NOT removing finalizer yet") + return fmt.Errorf("some error(s) occurred when deleting resources, NOT removing finalizer yet") } if err := c.removeFinalizer(); err != nil { return fmt.Errorf("done cleaning up, but error when removing finalizer: %v", err) diff --git a/pkg/cluster/volumes.go b/pkg/cluster/volumes.go index 7aa70a5d1..115474fef 100644 --- a/pkg/cluster/volumes.go +++ b/pkg/cluster/volumes.go @@ -39,7 +39,7 @@ func (c *Cluster) syncVolumes() error { } else { err = c.syncUnderlyingEBSVolume() if err != nil { - c.logger.Errorf("errors occured during EBS volume adjustments: %v", err) + c.logger.Errorf("errors occurred during EBS volume adjustments: %v", err) } } } From e871a167ed2ec5fd1fd78d7b1d0dd36ad57efb33 Mon Sep 17 00:00:00 2001 From: laiminhtrung1997 <68812829+laiminhtrung1997@users.noreply.github.com> Date: Fri, 29 May 2026 22:07:47 +0700 Subject: [PATCH 16/70] Add topologySpreadConstraints configuration to pod spec. (#2530) * Add topologySpreadConstraints configuration to pod spec. * Run update-codegen.sh to add deepcopy for new field to the api. * Reuse configured TopologySpreadConstraints for logical backup. * Remove x-kubernetes-preserve-unknown-fields and XPreserveUnknownFields. * Add topologySpreadConstraint example in the complete manifest. * Add support for helm chart. * Add documentation for topologySpreadConstraint. * Update e2e test to patch topologySpreadConstraints into the postgresqls manifest. * For e2e test, updated the PVC retention policy to remove redundant PVCs. * Fix e2e test, expected PVC count in end-to-end test after config changes. --- .../postgres-operator/crds/postgresqls.yaml | 17 +++ docs/user.md | 21 +++- e2e/tests/test_e2e.py | 107 ++++++++++++++++-- manifests/complete-postgres-manifest.yaml | 6 + manifests/postgresql.crd.yaml | 16 +++ pkg/apis/acid.zalan.do/v1/crds.go | 1 + pkg/apis/acid.zalan.do/v1/postgresql_type.go | 23 ++-- .../acid.zalan.do/v1/zz_generated.deepcopy.go | 7 ++ pkg/cluster/cluster.go | 5 + pkg/cluster/k8sres.go | 14 +++ pkg/cluster/k8sres_test.go | 53 +++++++++ 11 files changed, 245 insertions(+), 25 deletions(-) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 9b65b7663..785b220cc 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -4055,6 +4055,23 @@ spec: type: string type: object type: array + topologySpreadConstraints: + type: array + nullable: true + items: + type: object + properties: + maxskew: + type: integer + format: int32 + minimum: 1 + topologyKey: + type: string + whenUnsatisfiable: + type: string + enum: + - DoNotSchedule + - ScheduleAnyway useLoadBalancer: description: |- deprecated load balancer settings maintained for backward compatibility diff --git a/docs/user.md b/docs/user.md index 236b439a8..cf5089a89 100644 --- a/docs/user.md +++ b/docs/user.md @@ -714,7 +714,7 @@ but Kubernetes will not spin up the pod if the requested HugePages cannot be all For more information on HugePages in Kubernetes, see also [https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/](https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/) -## Use taints, tolerations and node affinity for dedicated PostgreSQL nodes +## Use taints, tolerations, node affinity and topology spread constraint for dedicated PostgreSQL nodes To ensure Postgres pods are running on nodes without any other application pods, you can use [taints and tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) @@ -755,6 +755,23 @@ spec: If you need to define a `nodeAffinity` for all your Postgres clusters use the `node_readiness_label` [configuration](administrator.md#node-readiness-labels). +If you need PostgreSQL Pods to run on separate nodes, you can use the +[topologySpreadConstraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) to control how they are distributed across your cluster. +This ensures they are spread among failure domains such as +regions, zones, nodes, or other user-defined topology domains. + +```yaml +apiVersion: "acid.zalan.do/v1" +kind: postgresql +metadata: + name: acid-minimal-cluster +spec: + topologySpreadConstraints: + - maxskew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule +``` + ## In-place major version upgrade Starting with Spilo 14, operator supports in-place major version upgrade to a @@ -1064,7 +1081,7 @@ spec: - all volumeSource: emptyDir: {} - sidecars: + sidecars: - name: "container-name" image: "company/image:tag" volumeMounts: diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 159c3cc79..7ce80a6c9 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -561,7 +561,7 @@ def compare_config(): pg_patch_config["spec"]["patroni"]["slots"][slot_to_change]["database"] = "bar" del pg_patch_config["spec"]["patroni"]["slots"][slot_to_remove] - + k8s.api.custom_objects_api.patch_namespaced_custom_object( "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_delete_slot_patch) @@ -578,7 +578,7 @@ def compare_config(): self.eventuallyEqual(lambda: self.query_database(leader.metadata.name, "postgres", get_slot_query%("database", slot_to_change))[0], "bar", "The replication slot cannot be updated", 10, 5) - + # make sure slot from Patroni didn't get deleted self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", get_slot_query%("slot_name", patroni_slot))), 1, "The replication slot from Patroni gets deleted", 10, 5) @@ -932,7 +932,7 @@ def test_ignored_annotations(self): }, } } - + 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 @@ -1370,7 +1370,7 @@ def test_persistent_volume_claim_retention_policy(self): } k8s.update_config(patch_scaled_policy_retain) self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") - + # decrease the number of instances k8s.api.custom_objects_api.patch_namespaced_custom_object( 'acid.zalan.do', 'v1', 'default', 'postgresqls', 'acid-minimal-cluster', pg_patch_scale_down_instances) @@ -1647,7 +1647,6 @@ def test_node_readiness_label(self): # toggle pod anti affinity to move replica away from master node self.assert_distributed_pods(master_nodes) - @timeout_decorator.timeout(TEST_TIMEOUT_SEC) def test_overwrite_pooler_deployment(self): pooler_name = 'acid-minimal-cluster-pooler' @@ -1800,7 +1799,7 @@ def test_password_rotation(self): }, } k8s.api.core_v1.patch_namespaced_secret( - name="foo-user.acid-minimal-cluster.credentials.postgresql.acid.zalan.do", + name="foo-user.acid-minimal-cluster.credentials.postgresql.acid.zalan.do", namespace="default", body=secret_fake_rotation) @@ -1817,7 +1816,7 @@ def test_password_rotation(self): "enable_password_rotation": "true", "inherited_annotations": "environment", "password_rotation_interval": "30", - "password_rotation_user_retention": "30", # should be set to 60 + "password_rotation_user_retention": "30", # should be set to 60 }, } k8s.update_config(enable_password_rotation) @@ -1886,7 +1885,7 @@ def test_password_rotation(self): self.assertTrue("environment" in db_user_secret.metadata.annotations, "Added annotation was not propagated to secret") # disable password rotation for all other users (foo_user) - # and pick smaller intervals to see if the third fake rotation user is dropped + # and pick smaller intervals to see if the third fake rotation user is dropped enable_password_rotation = { "data": { "enable_password_rotation": "false", @@ -2386,6 +2385,90 @@ def test_taint_based_eviction(self): # toggle pod anti affinity to move replica away from master node self.assert_distributed_pods(master_nodes) + @timeout_decorator.timeout(TEST_TIMEOUT_SEC) + def test_topology_spread_constraints(self): + ''' + Enable topologySpreadConstraints for pods + ''' + k8s = self.k8s + cluster_labels = "application=spilo,cluster-name=acid-minimal-cluster" + + # Verify we are in good state from potential previous tests + self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running") + + # patch the pvc retention policy to enable delete when scale down + patch_scaled_policy_delete = { + "data": { + "persistent_volume_claim_retention_policy": "when_deleted:retain,when_scaled:delete" + } + } + k8s.update_config(patch_scaled_policy_delete) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + + master_nodes, replica_nodes = k8s.get_cluster_nodes() + self.assertNotEqual(master_nodes, []) + self.assertNotEqual(replica_nodes, []) + + # Patch label to nodes for topologySpreadConstraints + patch_node_label = { + "metadata": { + "labels": { + "topology.kubernetes.io/zone": "zalando" + } + } + } + k8s.api.core_v1.patch_node(master_nodes[0], patch_node_label) + k8s.api.core_v1.patch_node(replica_nodes[0], patch_node_label) + + # Patch topologySpreadConstraint and scale-out postgresql pods to postgresqls manifest. + patch_topologySpreadConstraint_config = { + "spec": { + "numberOfInstances": 6, + "topologySpreadConstraint": [ + { + "maxskew": 1, + "topologyKey": "topology.kubernetes.io/zone", + "whenUnsatisfiable": "DoNotSchedule" + } + ] + } + } + k8s.api.custom_objects_api.patch_namespaced_custom_object( + "acid.zalan.do", "v1", "default", + "postgresqls", "acid-minimal-cluster", + patch_topologySpreadConstraint_config) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + self.eventuallyEqual(lambda: k8s.count_pods_with_label(cluster_labels), 6, "Postgresql StatefulSet are scale to 6") + self.eventuallyEqual(lambda: k8s.count_running_pods(), 6, "All pods are running") + + worker_node_1 = 0 + worker_node_2 = 0 + pods = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_labels) + for pod in pods.items: + if pod.spec.node_name == 'postgres-operator-e2e-tests-worker': + worker_node_1 += 1 + elif pod.spec.node_name == 'postgres-operator-e2e-tests-worker2': + worker_node_2 += 1 + + self.assertEqual(worker_node_1, worker_node_2) + self.assertEqual(worker_node_1, 3) + self.assertEqual(worker_node_2, 3) + + # Reset configurations + patch_topologySpreadConstraint_config = { + "spec": { + "numberOfInstances": 2, + "topologySpreadConstraint": [] + } + } + k8s.api.custom_objects_api.patch_namespaced_custom_object( + "acid.zalan.do", "v1", "default", + "postgresqls", "acid-minimal-cluster", + patch_topologySpreadConstraint_config) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + self.eventuallyEqual(lambda: k8s.count_pods_with_label(cluster_labels), 2, "Postgresql StatefulSet are scale to 2") + self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "All pods are running") + @timeout_decorator.timeout(TEST_TIMEOUT_SEC) def test_zz_cluster_deletion(self): ''' @@ -2461,7 +2544,7 @@ def test_zz_cluster_deletion(self): 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), 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") + self.eventuallyEqual(lambda: k8s.count_pvcs_with_label(cluster_label), 2, "PVCs were deleted although disabled in config") except timeout_decorator.TimeoutError: print('Operator log: {}'.format(k8s.get_operator_log())) @@ -2503,7 +2586,7 @@ def assert_distributed_pods(self, target_nodes, cluster_labels='cluster-name=aci # if nodes are different we can quit here if master_nodes[0] not in replica_nodes: - return True + return True # enable pod anti affintiy in config map which should trigger movement of replica patch_enable_antiaffinity = { @@ -2527,7 +2610,7 @@ def assert_distributed_pods(self, target_nodes, cluster_labels='cluster-name=aci } k8s.update_config(patch_disable_antiaffinity, "disable antiaffinity") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") - + k8s.wait_for_pod_start('spilo-role=replica,' + cluster_labels) k8s.wait_for_running_pods(cluster_labels, 2) @@ -2538,7 +2621,7 @@ def assert_distributed_pods(self, target_nodes, cluster_labels='cluster-name=aci # if nodes are different we can quit here for target_node in target_nodes: if (target_node not in master_nodes or target_node not in replica_nodes) and master_nodes[0] in replica_nodes: - print('Pods run on the same node') + print('Pods run on the same node') return False except timeout_decorator.TimeoutError: diff --git a/manifests/complete-postgres-manifest.yaml b/manifests/complete-postgres-manifest.yaml index 7b347a9c8..93797e0e1 100644 --- a/manifests/complete-postgres-manifest.yaml +++ b/manifests/complete-postgres-manifest.yaml @@ -232,6 +232,12 @@ spec: # values: # - enabled +# Add topology spread constraint to distribute PostgreSQL pods across all nodes labeled with "topology.kubernetes.io/zone". +# topologySpreadConstraint: +# - maxSkew: 1 +# topologyKey: topology.kubernetes.io/zone +# whenUnsatisfiable: DoNotSchedule + # Enables change data capture streams for defined database tables # streams: # - applicationId: test-app diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 39811824e..8cf360340 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -4056,6 +4056,22 @@ spec: type: string type: object type: array + topologySpreadConstraints: + type: array + nullable: true + items: + type: object + properties: + maxSkew: + type: integer + format: int32 + topologyKey: + type: string + whenUnsatisfiable: + type: string + enum: + - DoNotSchedule + - ScheduleAnyway useLoadBalancer: description: |- deprecated load balancer settings maintained for backward compatibility diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 3175f152a..1758a9b0d 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -56,6 +56,7 @@ var OperatorConfigCRDResourceColumns = []apiextv1.CustomResourceColumnDefinition } var min1 = 1.0 +var minLength1 int64 = 1 var minDisable = -1.0 // OperatorConfigCRDResourceValidation to check applied manifest parameters diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 1dadfd06c..40aa0fd18 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -92,17 +92,18 @@ type PostgresSpec struct { Clone *CloneDescription `json:"clone,omitempty"` // Note: usernames specified here as database owners must be declared // in the users key of the spec key. - Databases map[string]string `json:"databases,omitempty"` - PreparedDatabases map[string]PreparedDatabase `json:"preparedDatabases,omitempty"` - SchedulerName *string `json:"schedulerName,omitempty"` - NodeAffinity *v1.NodeAffinity `json:"nodeAffinity,omitempty"` - Tolerations []v1.Toleration `json:"tolerations,omitempty"` - Sidecars []Sidecar `json:"sidecars,omitempty"` - InitContainers []v1.Container `json:"initContainers,omitempty"` - PodPriorityClassName string `json:"podPriorityClassName,omitempty"` - ShmVolume *bool `json:"enableShmVolume,omitempty"` - EnableLogicalBackup bool `json:"enableLogicalBackup,omitempty"` - LogicalBackupRetention string `json:"logicalBackupRetention,omitempty"` + Databases map[string]string `json:"databases,omitempty"` + PreparedDatabases map[string]PreparedDatabase `json:"preparedDatabases,omitempty"` + SchedulerName *string `json:"schedulerName,omitempty"` + NodeAffinity *v1.NodeAffinity `json:"nodeAffinity,omitempty"` + TopologySpreadConstraints []v1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + Tolerations []v1.Toleration `json:"tolerations,omitempty"` + Sidecars []Sidecar `json:"sidecars,omitempty"` + InitContainers []v1.Container `json:"initContainers,omitempty"` + PodPriorityClassName string `json:"podPriorityClassName,omitempty"` + ShmVolume *bool `json:"enableShmVolume,omitempty"` + EnableLogicalBackup bool `json:"enableLogicalBackup,omitempty"` + LogicalBackupRetention string `json:"logicalBackupRetention,omitempty"` // +kubebuilder:validation:Pattern=`^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$` LogicalBackupSchedule string `json:"logicalBackupSchedule,omitempty"` StandbyCluster *StandbyDescription `json:"standby,omitempty"` 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 0fa4b1037..0779fac7a 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -801,6 +801,13 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { *out = new(corev1.NodeAffinity) (*in).DeepCopyInto(*out) } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations *out = make([]corev1.Toleration, len(*in)) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 488964b99..17e471b64 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -504,6 +504,11 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa needsRollUpdate = true reasons = append(reasons, "new statefulset's pod affinity does not match the current one") } + if !reflect.DeepEqual(c.Statefulset.Spec.Template.Spec.TopologySpreadConstraints, statefulSet.Spec.Template.Spec.TopologySpreadConstraints) { + needsReplace = true + needsRollUpdate = true + reasons = append(reasons, "new statefulset's pod topologySpreadConstraints does not match the current one") + } if len(c.Statefulset.Spec.Template.Spec.Tolerations) != len(statefulSet.Spec.Template.Spec.Tolerations) { needsReplace = true needsRollUpdate = true diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 2eb867f06..dcdbd9782 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -612,6 +612,13 @@ func generatePodAntiAffinity(podAffinityTerm v1.PodAffinityTerm, preferredDuring return podAntiAffinity } +func generateTopologySpreadConstraints(labels labels.Set, topologySpreadConstraints []v1.TopologySpreadConstraint) []v1.TopologySpreadConstraint { + for _, topologySpreadConstraint := range topologySpreadConstraints { + topologySpreadConstraint.LabelSelector = &metav1.LabelSelector{MatchLabels: labels} + } + return topologySpreadConstraints +} + func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]string) []v1.Toleration { // allow to override tolerations by postgresql manifest if len(*tolerationsSpec) > 0 { @@ -817,6 +824,7 @@ func (c *Cluster) generatePodTemplate( initContainers []v1.Container, sidecarContainers []v1.Container, sharePgSocketWithSidecars *bool, + topologySpreadConstraintsSpec []v1.TopologySpreadConstraint, tolerationsSpec *[]v1.Toleration, nodeAffinity *v1.Affinity, schedulerName *string, @@ -887,6 +895,8 @@ func (c *Cluster) generatePodTemplate( podSpec.PriorityClassName = priorityClassName } + podSpec.TopologySpreadConstraints = generateTopologySpreadConstraints(labels, topologySpreadConstraintsSpec) + if sharePgSocketWithSidecars != nil && *sharePgSocketWithSidecars { addVarRunVolume(&podSpec) } @@ -1469,6 +1479,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef initContainers, sidecarContainers, c.OpConfig.SharePgSocketWithSidecars, + spec.TopologySpreadConstraints, &tolerationSpec, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity), spec.SchedulerName, @@ -2346,6 +2357,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration) + topologySpreadConstraintsSpec := generateTopologySpreadConstraints(labels, spec.TopologySpreadConstraints) + // re-use the method that generates DB pod templates if podTemplate, err = c.generatePodTemplate( c.Namespace, @@ -2355,6 +2368,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { []v1.Container{}, []v1.Container{}, util.False(), + topologySpreadConstraintsSpec, &tolerationsSpec, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil), nil, diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 62481c7e3..9226c27ac 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -4272,3 +4272,56 @@ func TestGenerateCapabilities(t *testing.T) { } } } + +func TestTopologySpreadConstraints(t *testing.T) { + clusterName := "acid-test-cluster" + namespace := "default" + labelSelector := &metav1.LabelSelector{ + MatchLabels: cluster.labelsSet(true), + } + + pg := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: namespace, + }, + Spec: acidv1.PostgresSpec{ + NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + Volume: acidv1.Volume{ + Size: "1G", + }, + TopologySpreadConstraints: []v1.TopologySpreadConstraint{ + { + MaxSkew: 1, + TopologyKey: "topology.kubernetes.io/zone", + WhenUnsatisfiable: v1.DoNotSchedule, + LabelSelector: labelSelector, + }, + }, + }, + } + + cluster := New( + Config{ + OpConfig: config.Config{ + PodManagementPolicy: "ordered_ready", + }, + }, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder) + cluster.Name = clusterName + cluster.Namespace = namespace + cluster.labelsSet(true) + + s, err := cluster.generateStatefulSet(&pg.Spec) + assert.NoError(t, err) + assert.Contains(t, s.Spec.Template.Spec.TopologySpreadConstraints, v1.TopologySpreadConstraint{ + MaxSkew: int32(1), + TopologyKey: "topology.kubernetes.io/zone", + WhenUnsatisfiable: v1.DoNotSchedule, + LabelSelector: labelSelector, + }, + ) +} From f988e4cf0eef096e084e2de41e217beb0ccf356d Mon Sep 17 00:00:00 2001 From: thoro Date: Mon, 1 Jun 2026 09:18:20 +0200 Subject: [PATCH 17/70] Fix deletion timestamp handling for clusters with finalizers (#3015) When a Postgres cluster has a finalizer, deleting it sets a DeletionTimestamp but doesn't remove the object until the finalizer is cleared. The operator was not properly handling these DeletionTimestamp changes: 1. postgresqlUpdate() was filtering out events where only DeletionTimestamp changed (it only checked Spec and Annotations), causing the delete to never be processed. 2. EventUpdate case in processEvent() didn't check for DeletionTimestamp, so even if the event reached the processor, it would run Update() instead of Delete(). 3. removeFinalizer() used a cached object with stale resourceVersion, causing "object has been modified" errors. Fixes: - Add explicit DeletionTimestamp check in postgresqlUpdate() to queue the event - Add DeletionTimestamp check in EventUpdate to call Delete() when set - Fetch latest object from API before removing finalizer to avoid conflicts Co-authored-by: Felix Kunde --- pkg/cluster/cluster.go | 46 ++++++++++++++++++------------------ pkg/controller/controller.go | 2 +- pkg/controller/postgresql.go | 39 +++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 17e471b64..1c3ad5295 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -3,6 +3,7 @@ package cluster // Postgres CustomResourceDefinition object i.e. Spilo import ( + "context" "database/sql" "encoding/json" "fmt" @@ -71,7 +72,7 @@ type kubeResources struct { CriticalOpPodDisruptionBudget *policyv1.PodDisruptionBudget LogicalBackupJob *batchv1.CronJob Streams map[string]*zalandov1.FabricEventStream - //Pods are treated separately + // Pods are treated separately } // Cluster describes postgresql cluster @@ -96,7 +97,7 @@ type Cluster struct { teamsAPIClient teams.Interface oauthTokenGetter OAuthTokenGetter - KubeClient k8sutil.KubernetesClient //TODO: move clients to the better place? + KubeClient k8sutil.KubernetesClient // TODO: move clients to the better place? currentProcess Process processMu sync.RWMutex // protects the current operation for reporting, no need to hold the master mutex specMu sync.RWMutex // protects the spec for reporting, no need to hold the master mutex @@ -150,7 +151,8 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres PatroniEndpoints: make(map[string]*v1.Endpoints), PatroniConfigMaps: make(map[string]*v1.ConfigMap), VolumeClaims: make(map[types.UID]*v1.PersistentVolumeClaim), - Streams: make(map[string]*zalandov1.FabricEventStream)}, + Streams: make(map[string]*zalandov1.FabricEventStream), + }, userSyncStrategy: users.DefaultUserSyncStrategy{ PasswordEncryption: passwordEncryption, RoleDeletionSuffix: cfg.OpConfig.RoleDeletionSuffix, @@ -445,7 +447,7 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa var match, needsRollUpdate, needsReplace bool match = true - //TODO: improve me + // TODO: improve me if *c.Statefulset.Spec.Replicas != *statefulSet.Spec.Replicas { match = false reasons = append(reasons, "new statefulset's number of replicas does not match the current one") @@ -682,7 +684,6 @@ func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.Resourc } } return true - } func compareEnv(a, b []v1.EnvVar) bool { @@ -717,9 +718,7 @@ func compareEnv(a, b []v1.EnvVar) bool { } func compareSpiloConfiguration(configa, configb string) bool { - var ( - oa, ob spiloConfiguration - ) + var oa, ob spiloConfiguration var err error err = json.Unmarshal([]byte(configa), &oa) @@ -828,7 +827,6 @@ func (c *Cluster) compareAnnotations(old, new map[string]string, removedList *[] } return reason != "", reason - } func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) { @@ -905,7 +903,7 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) *compareLog } func (c *Cluster) comparePodDisruptionBudget(cur, new *policyv1.PodDisruptionBudget) (bool, string) { - //TODO: improve comparison + // TODO: improve comparison if !reflect.DeepEqual(new.Spec, cur.Spec) { return false, "new PDB's spec does not match the current one" } @@ -954,8 +952,17 @@ func (c *Cluster) removeFinalizer() error { } c.logger.Infof("removing finalizer %s", finalizerName) - finalizers := util.RemoveString(c.ObjectMeta.Finalizers, finalizerName) - newSpec, err := c.KubeClient.SetFinalizer(c.clusterName(), c.DeepCopy(), finalizers) + + // Fetch the latest version of the object to avoid resourceVersion conflicts + clusterName := c.clusterName() + latestPg, err := c.KubeClient.PostgresqlsGetter.Postgresqls(clusterName.Namespace).Get( + context.TODO(), clusterName.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("error fetching latest postgresql for finalizer removal: %v", err) + } + + finalizers := util.RemoveString(latestPg.ObjectMeta.Finalizers, finalizerName) + newSpec, err := c.KubeClient.SetFinalizer(clusterName, latestPg, finalizers) if err != nil { return fmt.Errorf("error removing finalizer: %v", err) } @@ -1078,7 +1085,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { } c.logger.Debug("syncing secrets") - //TODO: mind the secrets of the deleted/new users + // TODO: mind the secrets of the deleted/new users if err := c.syncSecrets(); err != nil { c.logger.Errorf("could not sync secrets: %v", err) updateFailed = true @@ -1116,7 +1123,6 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { // logical backup job func() { - // create if it did not exist if !oldSpec.Spec.EnableLogicalBackup && newSpec.Spec.EnableLogicalBackup { c.logger.Debug("creating backup cron job") @@ -1144,7 +1150,6 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { updateFailed = true } } - }() // Roles and Databases @@ -1221,7 +1226,7 @@ func syncResources(a, b *v1.ResourceRequirements) bool { // before the pods, it will be re-created by the current master pod and will remain, obstructing the // creation of the new cluster with the same name. Therefore, the endpoints should be deleted last. func (c *Cluster) Delete() error { - var anyErrors = false + anyErrors := false c.mu.Lock() defer c.mu.Unlock() c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Delete", "Started deletion of cluster resources") @@ -1312,7 +1317,6 @@ func (c *Cluster) NeedsRepair() (bool, acidv1.PostgresStatus) { c.specMu.RLock() defer c.specMu.RUnlock() return !c.Status.Success(), c.Status - } // ReceivePodEvent is called back by the controller in order to add the cluster's pod event to the queue. @@ -1421,7 +1425,6 @@ func (c *Cluster) initSystemUsers() { } func (c *Cluster) initPreparedDatabaseRoles() error { - if c.Spec.PreparedDatabases != nil && len(c.Spec.PreparedDatabases) == 0 { // TODO: add option to disable creating such a default DB c.Spec.PreparedDatabases = map[string]acidv1.PreparedDatabase{strings.Replace(c.Name, "-", "_", -1): {}} } @@ -1487,10 +1490,9 @@ func (c *Cluster) initPreparedDatabaseRoles() error { } func (c *Cluster) initDefaultRoles(defaultRoles map[string]string, admin, prefix, searchPath, secretNamespace string) error { - for defaultRole, inherits := range defaultRoles { namespace := c.Namespace - //if namespaced secrets are allowed + // if namespaced secrets are allowed if secretNamespace != "" { if c.Config.OpConfig.EnableCrossNamespaceSecret { namespace = secretNamespace @@ -1558,7 +1560,7 @@ func (c *Cluster) initRobotUsers() error { } } - //if namespaced secrets are allowed + // if namespaced secrets are allowed if c.Config.OpConfig.EnableCrossNamespaceSecret { if strings.Contains(username, ".") { splits := strings.Split(username, ".") @@ -1609,7 +1611,6 @@ func (c *Cluster) initAdditionalOwnerRoles() { func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) error { teamMembers, err := c.getTeamMembers(teamID) - if err != nil { return fmt.Errorf("could not get list of team members for team %q: %v", teamID, err) } @@ -1648,7 +1649,6 @@ func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) e } func (c *Cluster) initHumanUsers() error { - var clusterIsOwnedBySuperuserTeam bool superuserTeams := []string{} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 13e4017c8..089a71ab9 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -277,7 +277,7 @@ func (c *Controller) initRoleBinding() { }`, c.PodServiceAccount.Name, c.PodServiceAccount.Name, c.PodServiceAccount.Name) c.opConfig.PodServiceAccountRoleBindingDefinition = compactValue(stringValue) } - c.logger.Info("Parse role bindings") + // re-uses k8s internal parsing. See k8s client-go issue #193 for explanation decode := scheme.Codecs.UniversalDeserializer().Decode obj, groupVersionKind, err := decode([]byte(c.opConfig.PodServiceAccountRoleBindingDefinition), nil, nil) diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index 0725ffc1d..ab5e0d772 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -259,13 +259,26 @@ func (c *Controller) processEvent(event ClusterEvent) { lg.Infoln("cluster has been created") case EventUpdate: - lg.Infoln("update of the cluster started") - if !clusterFound { lg.Warningln("cluster does not exist") return } c.curWorkerCluster.Store(event.WorkerID, cl) + + // Check if this cluster has been marked for deletion + if !event.NewSpec.ObjectMeta.DeletionTimestamp.IsZero() { + lg.Infof("cluster has a DeletionTimestamp of %s, starting deletion now.", event.NewSpec.ObjectMeta.DeletionTimestamp.Format(time.RFC3339)) + if err = cl.Delete(); err != nil { + cl.Error = fmt.Sprintf("error deleting cluster and its resources: %v", err) + c.eventRecorder.Eventf(cl.GetReference(), v1.EventTypeWarning, "Delete", "%v", cl.Error) + lg.Error(cl.Error) + return + } + lg.Infoln("cluster has been deleted via update event") + return + } + + lg.Infoln("update of the cluster started") err = cl.Update(event.OldSpec, event.NewSpec) if err != nil { cl.Error = fmt.Sprintf("could not update cluster: %v", err) @@ -380,7 +393,6 @@ func (c *Controller) processClusterEventsQueue(idx int, stopCh <-chan struct{}, } func (c *Controller) warnOnDeprecatedPostgreSQLSpecParameters(spec *acidv1.PostgresSpec) { - deprecate := func(deprecated, replacement string) { c.logger.Warningf("parameter %q is deprecated. Consider setting %q instead", deprecated, replacement) } @@ -426,7 +438,7 @@ func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1. clusterError string ) - if informerOldSpec != nil { //update, delete + if informerOldSpec != nil { // update, delete uid = informerOldSpec.GetUID() clusterName = util.NameFromMeta(informerOldSpec.ObjectMeta) @@ -441,7 +453,7 @@ func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1. } else { clusterError = informerOldSpec.Error } - } else { //add, sync + } else { // add, sync uid = informerNewSpec.GetUID() clusterName = util.NameFromMeta(informerNewSpec.ObjectMeta) clusterError = informerNewSpec.Error @@ -552,7 +564,19 @@ func (c *Controller) postgresqlUpdate(prev, cur interface{}) { pgOld := c.postgresqlCheck(prev) pgNew := c.postgresqlCheck(cur) if pgOld != nil && pgNew != nil { - // Avoid the inifinite recursion for status updates + clusterName := util.NameFromMeta(pgNew.ObjectMeta) + + // Check if DeletionTimestamp was set (resource marked for deletion) + deletionTimestampChanged := pgOld.ObjectMeta.DeletionTimestamp.IsZero() && !pgNew.ObjectMeta.DeletionTimestamp.IsZero() + if deletionTimestampChanged { + c.logger.WithField("cluster-name", clusterName).Infof( + "UPDATE event: DeletionTimestamp set to %s, queueing event", + pgNew.ObjectMeta.DeletionTimestamp.Format(time.RFC3339)) + c.queueClusterEvent(pgOld, pgNew, EventUpdate) + return + } + + // Avoid the infinite recursion for status updates if reflect.DeepEqual(pgOld.Spec, pgNew.Spec) { if reflect.DeepEqual(pgNew.Annotations, pgOld.Annotations) { return @@ -591,7 +615,6 @@ or config maps. The operator does not sync accounts/role bindings after creation. */ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { - namespace := event.NewSpec.GetNamespace() if err := c.createPodServiceAccount(namespace); err != nil { @@ -605,7 +628,6 @@ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { } func (c *Controller) createPodServiceAccount(namespace string) error { - podServiceAccountName := c.opConfig.PodServiceAccountName _, err := c.KubeClient.ServiceAccounts(namespace).Get(context.TODO(), podServiceAccountName, metav1.GetOptions{}) if k8sutil.ResourceNotFound(err) { @@ -628,7 +650,6 @@ func (c *Controller) createPodServiceAccount(namespace string) error { } func (c *Controller) createRoleBindings(namespace string) error { - podServiceAccountName := c.opConfig.PodServiceAccountName podServiceAccountRoleBindingName := c.PodServiceAccountRoleBinding.Name From 5fb654f5f7a1335f17ed118db02b829a82a8b441 Mon Sep 17 00:00:00 2001 From: Kirill Petrov Date: Mon, 1 Jun 2026 13:04:48 +0500 Subject: [PATCH 18/70] Add support for passing extra command-line args via Helm values (#2892) * Add support for passing extra command-line args to the operator via Helm values This change introduces the ability to specify additional command-line arguments for the Postgres Operator via the "extraArgs" field in values.yaml. Documentation has been updated with details on new arguments "-kubeqps" and "-kubeburst" added before: https://github.com/zalando/postgres-operator/pull/2667. The chart version is bumped to 1.14.1 to reflect these changes. * reverted charts/postgres-operator/Chart.yaml --------- Co-authored-by: k.s.petrov Co-authored-by: Felix Kunde --- charts/postgres-operator/templates/deployment.yaml | 4 ++++ charts/postgres-operator/values.yaml | 3 +++ docs/reference/command_line_and_environment.md | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/charts/postgres-operator/templates/deployment.yaml b/charts/postgres-operator/templates/deployment.yaml index 395843942..c2eb0ba23 100644 --- a/charts/postgres-operator/templates/deployment.yaml +++ b/charts/postgres-operator/templates/deployment.yaml @@ -37,6 +37,10 @@ spec: - name: {{ .Chart.Name }} image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.extraArgs }} + args: +{{ toYaml .Values.extraArgs | indent 8 }} + {{- end }} env: {{- if .Values.enableJsonLogging }} - name: ENABLE_JSON_LOGGING diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index a1f4fa94c..858952f42 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -18,6 +18,9 @@ configTarget: "OperatorConfigurationCRD" # JSON logging format enableJsonLogging: false +# Command-line options for the operator +extraArgs: [] + # general configuration parameters configGeneral: # the deployment should create/update the CRDs diff --git a/docs/reference/command_line_and_environment.md b/docs/reference/command_line_and_environment.md index 35f47cabf..c8aab90b5 100644 --- a/docs/reference/command_line_and_environment.md +++ b/docs/reference/command_line_and_environment.md @@ -23,6 +23,12 @@ The following command-line options are supported for the operator: off can can be overridden by the aforementioned operator configuration option. +* **-kubeqps** + set the maximum number of Kubernetes API requests per second. Default is 10. + +* **-kubeburst** + set the burst limit for Kubernetes API requests, allowing temporary spikes beyond the configured QPS. Default is 20. + In addition to that, standard [glog flags](https://godoc.org/github.com/golang/glog) are also supported. For instance, one may want to add `-alsologtostderr` and `-v=8` to debug the From 26165be2d5dbd1e8aabb3063952c9d6bbfffadea Mon Sep 17 00:00:00 2001 From: Raphael Torquato <89878688+raphaeltorquat0@users.noreply.github.com> Date: Tue, 2 Jun 2026 05:45:44 -0300 Subject: [PATCH 19/70] test: add unit tests for validate() and NewFromMap() functions (#3086) This addresses issue #416 by adding comprehensive unit tests for the config utility functions: - TestValidate: Tests validation logic for MinInstances/MaxInstances, Workers count, ConnectionPooler instances, and user conflicts - TestNewFromMap: Tests config creation from map with defaults, custom values, duration/boolean/map/slice parsing, and panic behavior - TestMain: Sets OPERATOR_NAMESPACE env var for testing outside K8s The tests cover both valid configurations and error cases. Co-authored-by: Felix Kunde --- pkg/util/config/config_test.go | 306 +++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) diff --git a/pkg/util/config/config_test.go b/pkg/util/config/config_test.go index eb27d1bda..c5fd48d93 100644 --- a/pkg/util/config/config_test.go +++ b/pkg/util/config/config_test.go @@ -2,10 +2,19 @@ package config import ( "fmt" + "os" "reflect" + "strings" "testing" ) +func TestMain(m *testing.M) { + // Set OPERATOR_NAMESPACE to avoid log.Fatal in GetOperatorNamespace + // when running tests outside a Kubernetes pod + os.Setenv("OPERATOR_NAMESPACE", "default") + os.Exit(m.Run()) +} + var getMapPairsFromStringTest = []struct { in string expected []string @@ -29,3 +38,300 @@ func TestGetMapPairsFromString(t *testing.T) { } } } + +func int32Ptr(i int32) *int32 { + return &i +} + +func boolPtr(b bool) *bool { + return &b +} + +var validateTests = []struct { + description string + cfg Config + expectError bool + errorMsg string +}{ + { + description: "valid config", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 4, + }, + expectError: false, + }, + { + description: "min instances greater than max instances", + cfg: Config{ + Resources: Resources{ + MinInstances: 10, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 4, + }, + expectError: true, + errorMsg: "minimum number of instances", + }, + { + description: "workers set to zero", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 0, + }, + expectError: true, + errorMsg: "number of workers should be higher than 0", + }, + { + description: "connection pooler instances below minimum", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(0), + User: "pooler", + }, + Workers: 4, + }, + expectError: true, + errorMsg: "number of connection pooler instances", + }, + { + description: "connection pooler user same as super user", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "postgres", + }, + Workers: 4, + }, + expectError: true, + errorMsg: "connection pool user is not allowed to be the same as super user", + }, + { + description: "min and max instances both negative (disabled)", + cfg: Config{ + Resources: Resources{ + MinInstances: -1, + MaxInstances: -1, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 4, + }, + expectError: false, + }, +} + +func TestValidate(t *testing.T) { + for _, tt := range validateTests { + t.Run(tt.description, func(t *testing.T) { + err := validate(&tt.cfg) + if tt.expectError { + if err == nil { + t.Errorf("expected error containing %q, got nil", tt.errorMsg) + return + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + } + }) + } +} + +var newFromMapTests = []struct { + description string + input map[string]string + expectPanic bool + panicMsg string + validateFunc func(t *testing.T, cfg *Config) +}{ + { + description: "empty map uses defaults", + input: map[string]string{}, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.Workers != 8 { + t.Errorf("expected default Workers=8, got %d", cfg.Workers) + } + if cfg.SuperUsername != "postgres" { + t.Errorf("expected default SuperUsername=postgres, got %s", cfg.SuperUsername) + } + if cfg.ReplicationUsername != "standby" { + t.Errorf("expected default ReplicationUsername=standby, got %s", cfg.ReplicationUsername) + } + }, + }, + { + description: "custom values override defaults", + input: map[string]string{ + "workers": "16", + "super_username": "admin", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.Workers != 16 { + t.Errorf("expected Workers=16, got %d", cfg.Workers) + } + if cfg.SuperUsername != "admin" { + t.Errorf("expected SuperUsername=admin, got %s", cfg.SuperUsername) + } + }, + }, + { + description: "duration parsing", + input: map[string]string{ + "ready_wait_interval": "10s", + "ready_wait_timeout": "1m", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.ReadyWaitInterval.Seconds() != 10 { + t.Errorf("expected ReadyWaitInterval=10s, got %v", cfg.ReadyWaitInterval) + } + if cfg.ReadyWaitTimeout.Minutes() != 1 { + t.Errorf("expected ReadyWaitTimeout=1m, got %v", cfg.ReadyWaitTimeout) + } + }, + }, + { + description: "boolean parsing", + input: map[string]string{ + "enable_teams_api": "false", + "debug_logging": "false", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.EnableTeamsAPI != false { + t.Errorf("expected EnableTeamsAPI=false, got %v", cfg.EnableTeamsAPI) + } + if cfg.DebugLogging != false { + t.Errorf("expected DebugLogging=false, got %v", cfg.DebugLogging) + } + }, + }, + { + description: "map parsing", + input: map[string]string{ + "cluster_labels": "app:myapp,env:prod", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.ClusterLabels["app"] != "myapp" { + t.Errorf("expected ClusterLabels[app]=myapp, got %s", cfg.ClusterLabels["app"]) + } + if cfg.ClusterLabels["env"] != "prod" { + t.Errorf("expected ClusterLabels[env]=prod, got %s", cfg.ClusterLabels["env"]) + } + }, + }, + { + description: "slice parsing", + input: map[string]string{ + "inherited_labels": "label1,label2,label3", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + expected := []string{"label1", "label2", "label3"} + if !reflect.DeepEqual(cfg.InheritedLabels, expected) { + t.Errorf("expected InheritedLabels=%v, got %v", expected, cfg.InheritedLabels) + } + }, + }, + { + description: "invalid workers triggers validation panic", + input: map[string]string{ + "workers": "0", + }, + expectPanic: true, + panicMsg: "number of workers should be higher than 0", + }, + { + description: "invalid integer causes panic", + input: map[string]string{ + "workers": "invalid", + }, + expectPanic: true, + panicMsg: "invalid syntax", + }, +} + +func TestNewFromMap(t *testing.T) { + for _, tt := range newFromMapTests { + t.Run(tt.description, func(t *testing.T) { + if tt.expectPanic { + defer func() { + r := recover() + if r == nil { + t.Errorf("expected panic with message containing %q, but no panic occurred", tt.panicMsg) + return + } + errMsg := fmt.Sprintf("%v", r) + if !strings.Contains(errMsg, tt.panicMsg) { + t.Errorf("expected panic message containing %q, got %q", tt.panicMsg, errMsg) + } + }() + } + + cfg := NewFromMap(tt.input) + + if tt.expectPanic { + t.Errorf("expected panic but NewFromMap returned successfully") + return + } + + if tt.validateFunc != nil { + tt.validateFunc(t, cfg) + } + }) + } +} From 4177fa27bee3c1ac2f2d2cacd2b93d19c05677fe Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Tue, 2 Jun 2026 17:31:42 +0200 Subject: [PATCH 20/70] Provide liveness probe for CRD and config (#3089) * add LivenessProbe to both CRDs * auto-generate liveness probe for CRD * update topolgySpreadConstraint schema validation * Disable it for config map --- .../crds/operatorconfigurations.yaml | 153 ++++++++ .../postgres-operator/crds/postgresqls.yaml | 327 ++++++++++++++++- charts/postgres-operator/values.yaml | 13 + docs/reference/cluster_manifest.md | 4 + docs/reference/operator_parameters.md | 4 + manifests/operatorconfiguration.crd.yaml | 153 ++++++++ ...gresql-operator-default-configuration.yaml | 10 + manifests/postgresql.crd.yaml | 324 ++++++++++++++++- pkg/apis/acid.zalan.do/v1/crds.go | 126 +++++++ .../v1/operator_configuration_type.go | 1 + pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 328 ++++++++++++++++++ pkg/apis/acid.zalan.do/v1/postgresql_type.go | 2 +- .../acid.zalan.do/v1/zz_generated.deepcopy.go | 10 + pkg/cluster/cluster.go | 2 + pkg/cluster/k8sres.go | 15 + pkg/controller/operator_config.go | 1 + pkg/util/config/config.go | 1 + 17 files changed, 1459 insertions(+), 15 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 80ef38d25..961b5b655 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -282,6 +282,159 @@ spec: type: array items: type: string + liveness_probe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object master_pod_move_timeout: type: string default: "20m" diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 785b220cc..37bea3985 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -3245,6 +3245,159 @@ spec: - name type: object type: array + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object logicalBackupRetention: type: string logicalBackupSchedule: @@ -4056,22 +4209,180 @@ spec: type: object type: array topologySpreadConstraints: - type: array - nullable: true items: - type: object + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. properties: - maxskew: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. format: int32 - minimum: 1 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. type: string whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. type: string - enum: - - DoNotSchedule - - ScheduleAnyway + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array useLoadBalancer: description: |- deprecated load balancer settings maintained for backward compatibility diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 858952f42..82e9ac342 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -231,6 +231,19 @@ configKubernetes: # whether the Spilo container should run with additional permissions other than parent. # required by cron which needs setuid spilo_allow_privilege_escalation: true + + # liveness probe for the spilo pod + # liveness_probe: + # httpGet: + # scheme: HTTP + # path: /liveness + # port: 8008 + # initialDelaySeconds: 10 + # periodSeconds: 10 + # timeoutSeconds: 5 + # successThreshold: 1 + # failureThreshold: 3 + # storage resize strategy, available options are: ebs, pvc, off or mixed storage_resize_mode: pvc # pod toleration assigned to instances of every Postgres cluster diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index fd0660f57..a45350f38 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -85,6 +85,10 @@ These parameters are grouped directly under the `spec` key in the manifest. requires a custom Spilo image. Note the FSGroup of a Pod cannot be changed without recreating a new Pod. Optional. +* **livenessProbe** + Allows for adding a liveness probe to the Spilo container to detect if it's + running properly. + * **enableMasterLoadBalancer** boolean flag to override the operator defaults (set by the `enable_master_load_balancer` parameter) to define whether to enable the load diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index bd2ca7cf6..332742a16 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -334,6 +334,10 @@ configuration they are grouped under the `kubernetes` key. Postgres pods are [terminated forcefully](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) after this timeout. The default is `5m`. +* **liveness_probe** + Allows for adding a liveness probe to the Spilo container to detect if it's + running properly. Cannot be configured via ConfigMap. Default is empty. + * **custom_pod_annotations** This key/value map provides a list of annotations that get attached to each pod of a database created by the operator. If the annotation key is also provided diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index b5044b467..81bcd4381 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -280,6 +280,159 @@ spec: type: array items: type: string + liveness_probe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object master_pod_move_timeout: type: string default: "20m" diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 13dfd6977..88af48b66 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -87,6 +87,16 @@ configuration: # inherited_labels: # - application # - environment + # liveness_probe: + # httpGet: + # scheme: HTTP + # path: /liveness + # port: 8008 + # initialDelaySeconds: 10 + # periodSeconds: 10 + # timeoutSeconds: 5 + # successThreshold: 1 + # failureThreshold: 3 master_pod_move_timeout: 20m # node_readiness_label: # status: ready diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 8cf360340..b9652ef22 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -3246,6 +3246,159 @@ spec: - name type: object type: array + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object logicalBackupRetention: type: string logicalBackupSchedule: @@ -4057,21 +4210,180 @@ spec: type: object type: array topologySpreadConstraints: - type: array - nullable: true items: - type: object + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. type: string whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. type: string - enum: - - DoNotSchedule - - ScheduleAnyway + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array useLoadBalancer: description: |- deprecated load balancer settings maintained for backward compatibility diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 1758a9b0d..b6b58f072 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -372,6 +372,132 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ }, }, }, + "liveness_probe": { + Description: "Periodic probe of container liveness. Container will be restarted if the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + Type: "object", + Properties: map[string]apiextv1.JSONSchemaProps{ + "exec": { + Description: "One and only one of the following should be specified. Exec specifies the action to take.", + Type: "object", + Properties: map[string]apiextv1.JSONSchemaProps{ + "command": { + Description: "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + Type: "array", + Items: &apiextv1.JSONSchemaPropsOrArray{ + Schema: &apiextv1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + }, + }, + "failureThreshold": { + Description: "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + Type: "integer", + Format: "int32", + }, + "httpGet": { + Description: "HTTPGet specifies the http request to perform.", + Type: "object", + Required: []string{"port"}, + Properties: map[string]apiextv1.JSONSchemaProps{ + "host": { + Description: "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + Type: "string", + }, + "httpHeaders": { + Description: "Custom headers to set in the request. HTTP allows repeated headers.", + Type: "array", + Items: &apiextv1.JSONSchemaPropsOrArray{ + Schema: &apiextv1.JSONSchemaProps{ + Description: "HTTPHeader describes a custom header to be used in HTTP probes", + Type: "object", + Required: []string{"name", "value"}, + Properties: map[string]apiextv1.JSONSchemaProps{ + "name": { + Description: "The header field name", + Type: "string", + }, + "value": { + Description: "The header field value", + Type: "string", + }, + }, + }, + }, + }, + "path": { + Description: "Path to access on the HTTP server.", + Type: "string", + }, + "port": { + Description: "Name or number of the port to access on the container. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.", + AnyOf: []apiextv1.JSONSchemaProps{ + { + Type: "integer", + }, + { + Type: "string", + }, + }, + XIntOrString: true, + }, + "scheme": { + Description: "Scheme to use for connecting to the host. Defaults to HTTP.", + Type: "string", + }, + }, + }, + "initialDelaySeconds": { + Description: "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + Type: "integer", + Format: "int32", + }, + "periodSeconds": { + Description: "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + Type: "integer", + Format: "int32", + }, + "successThreshold": { + Description: "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + Type: "integer", + Format: "int32", + }, + "tcpSocket": { + Description: "TCPSocket specifies an action involving a TCP port. TCP hooks not yet supported TODO: implement a realistic TCP lifecycle hook", + Type: "object", + Required: []string{"port"}, + Properties: map[string]apiextv1.JSONSchemaProps{ + "host": { + Description: "Optional: Host name to connect to, defaults to the pod IP.", + Type: "string", + }, + "port": { + Description: "Number or name of the port to access on the container. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.", + XIntOrString: true, + AnyOf: []apiextv1.JSONSchemaProps{ + { + Type: "integer", + }, + { + Type: "string", + }, + }, + }, + }, + }, + "terminationGracePeriodSeconds": { + Description: "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + Type: "integer", + Format: "int64", + }, + "timeoutSeconds": { + Description: "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + Type: "integer", + Format: "int32", + }, + }, + }, "master_pod_move_timeout": { Type: "string", }, diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index 453d618d3..787a97253 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -61,6 +61,7 @@ type KubernetesMetaConfiguration struct { PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"` PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"` PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"` + LivenessProbe *v1.Probe `json:"liveness_probe"` SpiloPrivileged bool `json:"spilo_privileged,omitempty"` SpiloAllowPrivilegeEscalation *bool `json:"spilo_allow_privilege_escalation,omitempty"` SpiloRunAsUser *int64 `json:"spilo_runasuser,omitempty"` diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index 39811824e..b9652ef22 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -3246,6 +3246,159 @@ spec: - name type: object type: array + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object logicalBackupRetention: type: string logicalBackupSchedule: @@ -4056,6 +4209,181 @@ spec: type: string type: object type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array useLoadBalancer: description: |- deprecated load balancer settings maintained for backward compatibility diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 40aa0fd18..d67f23741 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -87,7 +87,6 @@ type PostgresSpec struct { NumberOfInstances int32 `json:"numberOfInstances"` // +kubebuilder:validation:Schemaless // +kubebuilder:validation:Type=array - // +kubebuilde:validation:items:Type=string MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"` Clone *CloneDescription `json:"clone,omitempty"` // Note: usernames specified here as database owners must be declared @@ -98,6 +97,7 @@ type PostgresSpec struct { NodeAffinity *v1.NodeAffinity `json:"nodeAffinity,omitempty"` TopologySpreadConstraints []v1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` Tolerations []v1.Toleration `json:"tolerations,omitempty"` + LivenessProbe *v1.Probe `json:"livenessProbe,omitempty"` Sidecars []Sidecar `json:"sidecars,omitempty"` InitContainers []v1.Container `json:"initContainers,omitempty"` PodPriorityClassName string `json:"podPriorityClassName,omitempty"` 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 0779fac7a..da4a18626 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -163,6 +163,11 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura *out = new(bool) **out = **in } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(corev1.Probe) + (*in).DeepCopyInto(*out) + } if in.SpiloAllowPrivilegeEscalation != nil { in, out := &in.SpiloAllowPrivilegeEscalation, &out.SpiloAllowPrivilegeEscalation *out = new(bool) @@ -815,6 +820,11 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(corev1.Probe) + (*in).DeepCopyInto(*out) + } if in.Sidecars != nil { in, out := &in.Sidecars, &out.Sidecars *out = make([]Sidecar, len(*in)) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 1c3ad5295..e1183c915 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -625,6 +625,8 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe func(a, b v1.Container) bool { return a.Name != b.Name }), newCheck("new %s's %s (index %d) readiness probe does not match the current one", func(a, b v1.Container) bool { return !reflect.DeepEqual(a.ReadinessProbe, b.ReadinessProbe) }), + newCheck("new %s's %s (index %d) liveness probe does not match the current one", + func(a, b v1.Container) bool { return !reflect.DeepEqual(a.LivenessProbe, b.LivenessProbe) }), newCheck("new %s's %s (index %d) ports do not match the current one", func(a, b v1.Container) bool { return !comparePorts(a.Ports, b.Ports) }), newCheck("new %s's %s (index %d) resources do not match the current ones", diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index dcdbd9782..0db805001 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -1299,6 +1299,19 @@ func generateSpiloReadinessProbe() *v1.Probe { } } +func generateSpiloLivenessProbe(probe, defaultProbe *v1.Probe) *v1.Probe { + + if probe != nil { + return probe + } + + if defaultProbe != nil { + return defaultProbe + } + + return nil +} + func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.StatefulSet, error) { var ( @@ -1405,6 +1418,8 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef spiloContainer.ReadinessProbe = generateSpiloReadinessProbe() } + spiloContainer.LivenessProbe = generateSpiloLivenessProbe(spec.LivenessProbe, c.OpConfig.LivenessProbe) + // generate container specs for sidecars specified in the cluster manifest clusterSpecificSidecars := []v1.Container{} if len(spec.Sidecars) > 0 { diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 4df8a8bd2..2b9cd9065 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -86,6 +86,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap result.PodEnvironmentSecret = fromCRD.Kubernetes.PodEnvironmentSecret result.PodTerminateGracePeriod = util.CoalesceDuration(time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod), "5m") + result.LivenessProbe = fromCRD.Kubernetes.LivenessProbe result.SpiloPrivileged = fromCRD.Kubernetes.SpiloPrivileged result.SpiloAllowPrivilegeEscalation = util.CoalesceBool(fromCRD.Kubernetes.SpiloAllowPrivilegeEscalation, util.True()) result.SpiloRunAsUser = fromCRD.Kubernetes.SpiloRunAsUser diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 796594a89..a14022407 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -31,6 +31,7 @@ type Resources struct { PodLabelWaitTimeout time.Duration `name:"pod_label_wait_timeout" default:"10m"` PodDeletionWaitTimeout time.Duration `name:"pod_deletion_wait_timeout" default:"10m"` PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"` + LivenessProbe *v1.Probe `name:"-"` SpiloRunAsUser *int64 `name:"spilo_runasuser"` SpiloRunAsGroup *int64 `name:"spilo_runasgroup"` SpiloFSGroup *int64 `name:"spilo_fsgroup"` From bd2500da81d2397bb46d8b3347ba4a2504de8e97 Mon Sep 17 00:00:00 2001 From: Zakaria Amine Date: Wed, 3 Jun 2026 12:43:34 +0200 Subject: [PATCH 21/70] refactor(controller): use kubernetes informers provided by client-go (#3080) * refactor(controller): use kubernetes informers provided by client-go * forgot assigning struct field member --------- Co-authored-by: Felix Kunde --- pkg/controller/controller.go | 23 ++++------------------- pkg/controller/node.go | 22 ---------------------- pkg/controller/pod.go | 25 ------------------------- pkg/util/k8sutil/k8sutil.go | 2 ++ 4 files changed, 6 insertions(+), 66 deletions(-) diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 089a71ab9..93a9c7f3e 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -26,6 +26,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + informers_core_v1 "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes/scheme" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" @@ -401,16 +402,8 @@ func (c *Controller) initSharedInformers() { } // Pods - podLw := &cache.ListWatch{ - ListFunc: c.podListFunc, - WatchFunc: c.podWatchFunc, - } - - c.podInformer = cache.NewSharedIndexInformer( - podLw, - &v1.Pod{}, - constants.QueueResyncPeriodPod, - cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) + c.podInformer = informers_core_v1.NewPodInformer(c.KubeClient.Clientset, + c.opConfig.WatchedNamespace, constants.QueueResyncPeriodPod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) c.podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: c.podAdd, @@ -419,15 +412,7 @@ func (c *Controller) initSharedInformers() { }) // Kubernetes Nodes - nodeLw := &cache.ListWatch{ - ListFunc: c.nodeListFunc, - WatchFunc: c.nodeWatchFunc, - } - - c.nodesInformer = cache.NewSharedIndexInformer( - nodeLw, - &v1.Node{}, - constants.QueueResyncPeriodNode, + c.nodesInformer = informers_core_v1.NewNodeInformer(c.KubeClient.Clientset, constants.QueueResyncPeriodNode, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) c.nodesInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ diff --git a/pkg/controller/node.go b/pkg/controller/node.go index 2836b4f7f..730a9133b 100644 --- a/pkg/controller/node.go +++ b/pkg/controller/node.go @@ -9,33 +9,11 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" "github.com/zalando/postgres-operator/pkg/cluster" "github.com/zalando/postgres-operator/pkg/util" ) -func (c *Controller) nodeListFunc(options metav1.ListOptions) (runtime.Object, error) { - opts := metav1.ListOptions{ - Watch: options.Watch, - ResourceVersion: options.ResourceVersion, - TimeoutSeconds: options.TimeoutSeconds, - } - - return c.KubeClient.Nodes().List(context.TODO(), opts) -} - -func (c *Controller) nodeWatchFunc(options metav1.ListOptions) (watch.Interface, error) { - opts := metav1.ListOptions{ - Watch: options.Watch, - ResourceVersion: options.ResourceVersion, - TimeoutSeconds: options.TimeoutSeconds, - } - - return c.KubeClient.Nodes().Watch(context.TODO(), opts) -} - func (c *Controller) nodeAdd(obj interface{}) { node, ok := obj.(*v1.Node) if !ok { diff --git a/pkg/controller/pod.go b/pkg/controller/pod.go index 0defe88b1..1aaa307ea 100644 --- a/pkg/controller/pod.go +++ b/pkg/controller/pod.go @@ -1,12 +1,7 @@ package controller import ( - "context" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" "github.com/zalando/postgres-operator/pkg/cluster" "github.com/zalando/postgres-operator/pkg/spec" @@ -14,26 +9,6 @@ import ( "k8s.io/apimachinery/pkg/types" ) -func (c *Controller) podListFunc(options metav1.ListOptions) (runtime.Object, error) { - opts := metav1.ListOptions{ - Watch: options.Watch, - ResourceVersion: options.ResourceVersion, - TimeoutSeconds: options.TimeoutSeconds, - } - - return c.KubeClient.Pods(c.opConfig.WatchedNamespace).List(context.TODO(), opts) -} - -func (c *Controller) podWatchFunc(options metav1.ListOptions) (watch.Interface, error) { - opts := metav1.ListOptions{ - Watch: options.Watch, - ResourceVersion: options.ResourceVersion, - TimeoutSeconds: options.TimeoutSeconds, - } - - return c.KubeClient.Pods(c.opConfig.WatchedNamespace).Watch(context.TODO(), opts) -} - func (c *Controller) dispatchPodEvent(clusterName spec.NamespacedName, event cluster.PodEvent) { c.clustersMu.RLock() cluster, ok := c.clusters[clusterName] diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index c34faddd4..8515027ab 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -67,6 +67,7 @@ type KubernetesClient struct { zalandov1.FabricEventStreamsGetter RESTClient rest.Interface + Clientset *kubernetes.Clientset AcidV1ClientSet *zalandoclient.Clientset Zalandov1ClientSet *zalandoclient.Clientset } @@ -148,6 +149,7 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) { return kubeClient, fmt.Errorf("could not get clientset: %v", err) } + kubeClient.Clientset = client kubeClient.PodsGetter = client.CoreV1() kubeClient.ServicesGetter = client.CoreV1() kubeClient.EndpointsGetter = client.CoreV1() From a1bbfd9d9a2c215e54de800e81e91a16b35a4a87 Mon Sep 17 00:00:00 2001 From: Raphael Torquato <89878688+raphaeltorquat0@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:00:51 -0300 Subject: [PATCH 22/70] Fix bool config defaults when using OperatorConfiguration CRD (#3084) * Fix bool config defaults when using OperatorConfiguration CRD When using OperatorConfiguration CRD, boolean fields with default value of `true` (like `enable_database_access` and `debug_logging`) were incorrectly defaulting to `false` when not explicitly specified. This happened because Go initializes bool fields to `false`, and there was no coalesce logic to apply the intended defaults. The fix changes the CRD type fields from `bool` to `*bool` (pointer), allowing us to distinguish between "not specified" (nil) and "explicitly set to false". Then we use the existing `CoalesceBool` utility function to apply the correct defaults. Fixes #2575 * update generated code Updated DeepCopyInto method for OperatorDebugConfiguration to handle pointers for DebugLogging and EnableDBAccess. --------- Co-authored-by: Felix Kunde --- .../acid.zalan.do/v1/operator_configuration_type.go | 4 ++-- pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go | 12 +++++++++++- pkg/controller/operator_config.go | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) 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 787a97253..0087e5850 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -168,8 +168,8 @@ type AWSGCPConfiguration struct { // OperatorDebugConfiguration defines options for the debug mode type OperatorDebugConfiguration struct { - DebugLogging bool `json:"debug_logging,omitempty"` - EnableDBAccess bool `json:"enable_database_access,omitempty"` + DebugLogging *bool `json:"debug_logging,omitempty"` + EnableDBAccess *bool `json:"enable_database_access,omitempty"` } // TeamsAPIConfiguration defines the configuration of TeamsAPI 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 da4a18626..3fdc31fa7 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -476,7 +476,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData out.Timeouts = in.Timeouts in.LoadBalancer.DeepCopyInto(&out.LoadBalancer) out.AWSGCP = in.AWSGCP - out.OperatorDebug = in.OperatorDebug + in.OperatorDebug.DeepCopyInto(&out.OperatorDebug) in.TeamsAPI.DeepCopyInto(&out.TeamsAPI) out.LoggingRESTAPI = in.LoggingRESTAPI out.Scalyr = in.Scalyr @@ -532,6 +532,16 @@ func (in *OperatorConfigurationList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperatorDebugConfiguration) DeepCopyInto(out *OperatorDebugConfiguration) { *out = *in + if in.DebugLogging != nil { + in, out := &in.DebugLogging, &out.DebugLogging + *out = new(bool) + **out = **in + } + if in.EnableDBAccess != nil { + in, out := &in.EnableDBAccess, &out.EnableDBAccess + *out = new(bool) + **out = **in + } return } diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 2b9cd9065..e304c14a5 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -215,8 +215,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.LogicalBackupMemoryLimit = fromCRD.LogicalBackup.MemoryLimit // debug config - result.DebugLogging = fromCRD.OperatorDebug.DebugLogging - result.EnableDBAccess = fromCRD.OperatorDebug.EnableDBAccess + result.DebugLogging = *util.CoalesceBool(fromCRD.OperatorDebug.DebugLogging, util.True()) + result.EnableDBAccess = *util.CoalesceBool(fromCRD.OperatorDebug.EnableDBAccess, util.True()) // Teams API config result.EnableTeamsAPI = fromCRD.TeamsAPI.EnableTeamsAPI From 40b6c68443aaafe0e2411f24302784ef763a4819 Mon Sep 17 00:00:00 2001 From: Kadaffy Talavera Date: Thu, 4 Jun 2026 20:55:04 +0200 Subject: [PATCH 23/70] fix: set password encryption default to scram-sha-256 (#2962) * fix: set password encryption default to scram-sha-256 According to the Postgres official documentation, md5 passwords is deprecated in favor of scram-sha-256 encryption. The change in this PR updates the default encryption to the new postgres default. Documentation link: https://www.postgresql.org/docs/18/auth-password.html >Warning: Support for MD5-encrypted passwords is deprecated and will be removed in a future release of PostgreSQL. Signed-off-by: Kadaffy Talavera * fix: update user documentation about password encryption Signed-off-by: Kadaffy Talavera * Apply suggestion from @FxKu --------- Signed-off-by: Kadaffy Talavera Co-authored-by: Felix Kunde --- docs/user.md | 5 +---- pkg/cluster/cluster.go | 2 +- pkg/cluster/cluster_test.go | 8 ++++---- pkg/cluster/k8sres.go | 2 +- pkg/cluster/k8sres_test.go | 12 ++++++------ pkg/util/util.go | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/user.md b/docs/user.md index cf5089a89..293ddf8c2 100644 --- a/docs/user.md +++ b/docs/user.md @@ -96,10 +96,7 @@ psql -U postgres -h localhost -p 6432 ## Password encryption -Passwords are encrypted with `md5` hash generation by default. However, it is -possible to use the more recent `scram-sha-256` method by changing the -`password_encryption` parameter in the Postgres config. You can define it -directly from the cluster manifest: +Passwords are encrypted using the `scram-sha-256` hashing method by default. Other methods can be configured by changing the `password_encryption` parameter in the cluster manifest: ```yaml apiVersion: "acid.zalan.do/v1" diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index e1183c915..b7a7b8e56 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -135,7 +135,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres }) passwordEncryption, ok := pgSpec.Spec.PostgresqlParam.Parameters["password_encryption"] if !ok { - passwordEncryption = "md5" + passwordEncryption = "scram-sha-256" } cluster := &Cluster{ diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 56b9640ef..84d75cb0a 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -33,8 +33,8 @@ const ( replicationUserName = "standby" poolerUserName = "pooler" adminUserName = "admin" - exampleSpiloConfig = `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}` - spiloConfigDiff = `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}` + exampleSpiloConfig = `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}` + spiloConfigDiff = `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}` ) var logger = logrus.New().WithField("test", "cluster") @@ -1186,11 +1186,11 @@ func TestCompareSpiloConfiguration(t *testing.T) { ExpectedResult bool }{ { - `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`, + `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`, true, }, { - `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"200","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`, + `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"200","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`, true, }, { diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 0db805001..866eeb752 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -366,7 +366,7 @@ func generateSpiloJSONConfiguration(pg *acidv1.PostgresqlParam, patroni *acidv1. config.Bootstrap = pgBootstrap{} - config.Bootstrap.Initdb = []interface{}{map[string]string{"auth-host": "md5"}, + config.Bootstrap.Initdb = []interface{}{map[string]string{"auth-host": "scram-sha-256"}, map[string]string{"auth-local": "trust"}} initdbOptionNames := []string{} diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 9226c27ac..2010de067 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -79,7 +79,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { PamRoleName: "zalandos", }, }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"dcs":{}}}`, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{}}}`, }, { subtest: "Patroni configured", @@ -90,7 +90,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { "locale": "en_US.UTF-8", "data-checksums": "true", }, - PgHba: []string{"hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5"}, + PgHba: []string{"hostssl all all 0.0.0.0/0 scram-sha-256", "host all all 0.0.0.0/0 scram-sha-256"}, TTL: 30, LoopWait: 10, RetryTimeout: 10, @@ -102,7 +102,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { FailsafeMode: util.True(), }, opConfig: &config.Config{}, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin","pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin","pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`, }, { subtest: "Patroni failsafe_mode configured globally", @@ -111,7 +111,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { opConfig: &config.Config{ EnablePatroniFailsafeMode: util.True(), }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`, }, { subtest: "Patroni failsafe_mode configured globally, disabled for cluster", @@ -122,7 +122,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { opConfig: &config.Config{ EnablePatroniFailsafeMode: util.True(), }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":false}}}`, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":false}}}`, }, { subtest: "Patroni failsafe_mode disabled globally, configured for cluster", @@ -133,7 +133,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { opConfig: &config.Config{ EnablePatroniFailsafeMode: util.False(), }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`, }, } for _, tt := range tests { diff --git a/pkg/util/util.go b/pkg/util/util.go index 4b3aafc63..993a773c4 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -87,7 +87,7 @@ func NewEncryptor(encryption string) *Encryptor { } hasher, ok := m[encryption] if !ok { - hasher = e.PGUserPasswordMD5 + hasher = e.PGUserPasswordScramSHA256 } e.encrypt = hasher return &e From f0b5d3725c04d7a9286200642f2f92955fcf138c Mon Sep 17 00:00:00 2001 From: Jorge Solorzano Date: Wed, 10 Jun 2026 11:32:13 +0200 Subject: [PATCH 24/70] Bump github.com/lib/pq from v1.11.2 to v1.12.3 (#3042) Co-authored-by: Felix Kunde --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e0e0b1956..9efa24150 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/aws/aws-sdk-go v1.55.8 github.com/golang/mock v1.6.0 - github.com/lib/pq v1.11.2 + github.com/lib/pq v1.12.3 github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d github.com/pkg/errors v0.9.1 github.com/r3labs/diff v1.1.0 diff --git a/go.sum b/go.sum index a1fa39389..0d0ebb7d2 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= -github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= From 873dd548ffcd1956b709e75ebe4e1eb555dcce2f Mon Sep 17 00:00:00 2001 From: Raphael Torquato <89878688+raphaeltorquat0@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:18:54 -0300 Subject: [PATCH 25/70] Add cluster_labels and annotations to logical backup CronJob and Jobs (#3085) * Add cluster_labels and annotations to logical backup CronJob and Jobs When using the logical backup feature, the CronJob and its created Jobs were missing the cluster_labels and annotations that are applied to other cluster resources. This made it difficult to filter or identify backup jobs using the same labels as other cluster components. Changes: - Added ObjectMeta with labels and annotations to JobTemplateSpec - Updated CronJob ObjectMeta to use the merged labels (including 'application: spilo-logical-backup') - Updated tests to expect the new labels --- e2e/tests/k8s_api.py | 2 +- pkg/cluster/cluster.go | 10 ++++++++++ pkg/cluster/k8sres.go | 8 ++++++-- pkg/cluster/k8sres_test.go | 12 ++++++------ pkg/cluster/sync.go | 10 ++++++++++ pkg/cluster/util.go | 8 ++++++++ 6 files changed, 41 insertions(+), 9 deletions(-) diff --git a/e2e/tests/k8s_api.py b/e2e/tests/k8s_api.py index 1f42ad4bc..0ef3d6315 100644 --- a/e2e/tests/k8s_api.py +++ b/e2e/tests/k8s_api.py @@ -240,7 +240,7 @@ def wait_for_namespace_creation(self, namespace='default'): time.sleep(self.RETRY_TIMEOUT_SEC) def get_logical_backup_job(self, namespace='default'): - return self.api.batch_v1.list_namespaced_cron_job(namespace, label_selector="application=spilo") + return self.api.batch_v1.list_namespaced_cron_job(namespace, label_selector="application=spilo-logical-backup") def wait_for_logical_backup_job(self, expected_num_of_jobs): while (len(self.get_logical_backup_job().items) != expected_num_of_jobs): diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index b7a7b8e56..95f16d922 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -893,6 +893,16 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) *compareLog reasons = append(reasons, fmt.Sprintf("new job's env PG_VERSION %q does not match the current one %q", newPgVersion, curPgVersion)) } + if !reflect.DeepEqual(cur.Labels, new.Labels) { + match = false + reasons = append(reasons, "new job's labels do not match the current ones") + } + + if !reflect.DeepEqual(cur.Spec.JobTemplate.Labels, new.Spec.JobTemplate.Labels) { + match = false + reasons = append(reasons, "new job's template labels do not match the current ones") + } + needsReplace := false contReasons := make([]string, 0) needsReplace, contReasons = c.compareContainers("cronjob container", cur.Spec.JobTemplate.Spec.Template.Spec.Containers, new.Spec.JobTemplate.Spec.Template.Spec.Containers, needsReplace, contReasons) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 866eeb752..8d3a40d9a 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -2414,6 +2414,10 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { // configure a cron job jobTemplateSpec := batchv1.JobTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: c.annotationsSet(annotations), + }, Spec: jobSpec, } @@ -2426,8 +2430,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { ObjectMeta: metav1.ObjectMeta{ Name: c.getLogicalBackupJobName(), Namespace: c.Namespace, - Labels: c.labelsSet(true), - Annotations: c.annotationsSet(nil), + Labels: labels, + Annotations: c.annotationsSet(annotations), OwnerReferences: c.ownerReferences(), }, Spec: batchv1.CronJobSpec{ diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 2010de067..ef408da4d 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -3875,7 +3875,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) { ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")}, ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("500Mi")}, }, - expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId}, + expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId}, expectedAnnotation: nil, }, { @@ -3900,7 +3900,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) { ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("10m"), Memory: k8sutil.StringToPointer("50Mi")}, ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("300m"), Memory: k8sutil.StringToPointer("300Mi")}, }, - expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId}, + expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId}, expectedAnnotation: nil, }, { @@ -3923,7 +3923,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) { ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("50m"), Memory: k8sutil.StringToPointer("100Mi")}, ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("250m"), Memory: k8sutil.StringToPointer("500Mi")}, }, - expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId}, + expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId}, expectedAnnotation: nil, }, { @@ -3946,7 +3946,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) { ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("200Mi")}, ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("200Mi")}, }, - expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId}, + expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId}, expectedAnnotation: nil, }, { @@ -3968,7 +3968,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) { ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")}, ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("500Mi")}, }, - expectedLabel: map[string]string{"labelKey": "labelValue", "cluster-name": clusterName, "team": teamId}, + expectedLabel: map[string]string{"application": "spilo-logical-backup", "labelKey": "labelValue", "cluster-name": clusterName, "team": teamId}, expectedAnnotation: nil, }, { @@ -3990,7 +3990,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) { ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")}, ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("500Mi")}, }, - expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId}, + expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId}, expectedAnnotation: map[string]string{"annotationKey": "annotationValue"}, }, } diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 7c478477a..e15b5fedc 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -1769,6 +1769,16 @@ func (c *Cluster) syncLogicalBackupJob() error { } c.logger.Info("the logical backup job is synced") } + if !reflect.DeepEqual(job.Labels, desiredJob.Labels) { + patchData, err := metaLabelsPatch(desiredJob.Labels) + if err != nil { + return fmt.Errorf("could not form patch for the logical backup job %q labels: %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 labels of the logical backup job %q: %v", jobName, err) + } + } if changed, _ := c.compareAnnotations(job.Annotations, desiredJob.Annotations, nil); changed { patchData, err := metaAnnotationsPatch(desiredJob.Annotations) if err != nil { diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 9c830129d..cbcccd16e 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -167,6 +167,14 @@ func metaAnnotationsPatch(annotations map[string]string) ([]byte, error) { }{&meta}) } +func metaLabelsPatch(labels map[string]string) ([]byte, error) { + var meta metav1.ObjectMeta + meta.Labels = labels + return json.Marshal(struct { + ObjMeta interface{} `json:"metadata"` + }{&meta}) +} + func (c *Cluster) logPDBChanges(old, new *policyv1.PodDisruptionBudget, isUpdate bool, reason string) { if isUpdate { c.logger.Infof("pod disruption budget %q has been changed", util.NameFromMeta(old.ObjectMeta)) From ab740cf5e5cc9db973c93480345f262794bfad60 Mon Sep 17 00:00:00 2001 From: Allen Conlon Date: Thu, 11 Jun 2026 03:55:31 -0400 Subject: [PATCH 26/70] feat: add publish of helm chart to ghcr.io (#2853) Signed-off-by: Allen Conlon Co-authored-by: Felix Kunde --- .github/workflows/publish_ghcr_image.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/publish_ghcr_image.yaml b/.github/workflows/publish_ghcr_image.yaml index 2425e39b3..5a0c3b045 100644 --- a/.github/workflows/publish_ghcr_image.yaml +++ b/.github/workflows/publish_ghcr_image.yaml @@ -101,3 +101,15 @@ jobs: build-args: BASE_IMAGE=ubuntu:22.04 tags: "${{ steps.image_lb.outputs.BACKUP_IMAGE }}" platforms: linux/amd64,linux/arm64 + + - name: Build and push postgres-operator chart to ghcr + run: | + helm package charts/postgres-operator + helm push postgres-operator-*.tgz oci://${{ env.REGISTRY }}/zalando/charts + rm -rf postgres-operator-*.tgz + + - name: Build and push postgres-operator-ui chart to ghcr + run: | + helm package charts/postgres-operator-ui + helm push postgres-operator-ui-*.tgz oci://${{ env.REGISTRY }}/zalando/charts + rm -rf postgres-operator-ui-*.tgz From a71e6bdf7fba12769e213790e7103eeb7599eb8f Mon Sep 17 00:00:00 2001 From: Lucas Nikola Pape Date: Fri, 12 Jun 2026 08:02:26 +0000 Subject: [PATCH 27/70] feat: implement service type NodePort (#2986) feat: implement service type NodePort fix: handle LoadBalancer to NodePort service type transition move NodePort check before LoadBalancer and remove redundant nodePor add LB-specific DNS annotations again --- .../crds/operatorconfigurations.yaml | 12 ++ .../postgres-operator/crds/postgresqls.yaml | 20 ++ docs/administrator.md | 35 +++ docs/reference/cluster_manifest.md | 42 ++++ pkg/apis/acid.zalan.do/v1/crds.go | 12 ++ .../v1/operator_configuration_type.go | 29 ++- pkg/apis/acid.zalan.do/v1/postgresql_type.go | 12 ++ .../acid.zalan.do/v1/zz_generated.deepcopy.go | 40 ++++ pkg/cluster/cluster.go | 8 + pkg/cluster/cluster_test.go | 71 ++++-- pkg/cluster/connection_pooler.go | 39 +++- pkg/cluster/connection_pooler_test.go | 178 +++++++++++++++ pkg/cluster/k8sres.go | 48 ++++- pkg/cluster/k8sres_test.go | 203 +++++++++++++++++- pkg/cluster/resources.go | 9 +- pkg/controller/operator_config.go | 4 + pkg/util/config/config.go | 4 + 17 files changed, 728 insertions(+), 38 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 961b5b655..09356c476 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -610,6 +610,18 @@ spec: enable_replica_pooler_load_balancer: type: boolean default: false + enable_master_node_port: + type: boolean + default: false + enable_master_pooler_node_port: + type: boolean + default: false + enable_replica_node_port: + type: boolean + default: false + enable_replica_pooler_node_port: + type: boolean + default: false external_traffic_policy: type: string enum: diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 37bea3985..a52259a85 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -281,6 +281,26 @@ spec: type: boolean enableReplicaPoolerLoadBalancer: type: boolean + enableMasterNodePort: + type: boolean + masterNodePort: + type: integer + minimum: 0 + enableMasterPoolerNodePort: + type: boolean + masterPoolerNodePort: + type: integer + minimum: 0 + enableReplicaNodePort: + type: boolean + replicaNodePort: + type: integer + minimum: 0 + enableReplicaPoolerNodePort: + type: boolean + replicaPoolerNodePort: + type: integer + minimum: 0 enableShmVolume: type: boolean env: diff --git a/docs/administrator.md b/docs/administrator.md index e854775ce..ec0bc28dc 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -928,6 +928,41 @@ For the `external-dns.alpha.kubernetes.io/hostname` annotation the `-pooler` suffix will be appended to the cluster name used in the template which is defined in `master|replica_dns_name_format`. +## Node Ports + +Alternatively to Load Balancers Node Ports can be used. Kubernetes services with type +`NodePort` redirect traffic from a specified port on your kubernetes nodes to your service. +To expose your services to an external network with NodePorts you can set `enableMasterNodePort` and/or `enableReplicaNodePort` to `true` +in your cluster manifest. In the case any of these variables are omitted from the manifest, the operator configuration settings `enable_master_node_port` and `enable_replica_node_port` apply. +Note that the operator settings affect all Postgresql services running in all namespaces watched +by the operator. + +**Enabling a NodePort configuration will override the corresponding LoadBalancer configuration.** + +There are multiple options to specify service annotations that will be merged +with each other and override in the following order (where latter take +precedence): + +1. Globally configured `custom_service_annotations` +2. `serviceAnnotations` specified in the cluster manifest +3. `masterServiceAnnotations` and `replicaServiceAnnotations` specified in the cluster manifest + +Load-Balancer specific annotations are not applied. + +Node port services can also be configured for the [connection pooler](user.md#connection-pooler) pods +with the manifest flags `enableMasterPoolerNodePort` and/or `enableReplicaPoolerNodePort` or in the operator configuration with `enable_master_pooler_node_port` +and/or `enable_replica_pooler_node_port`. + +To configure which ports Kubernetes should use for your NodePort service you can configure ports in your cluster manifest +for each type: + +- masterNodePort +- masterPoolerNodePort +- replicaNodePort +- replicaPoolerNodePort + +When not defined or set to 0 kubernetes will choose a port for you from [your kubernetes cluster's configured range](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport). + ## Running periodic 'autorepair' scans of K8s objects The Postgres Operator periodically scans all K8s objects belonging to each diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index a45350f38..b216c1fb2 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -118,6 +118,48 @@ 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. +* **enableMasterNodePort** + boolean flag to override the operator defaults (set by the + `enable_master_node_port` parameter) to define whether to enable the node + port pointing to the Postgres primary. Optional. Overrides `enableMasterLoadBalancer`. + +* **enableMasterPoolerNodePort** + boolean flag to override the operator defaults (set by the + `enable_master_pooler_node_port` parameter) to define whether to enable + the node port for master pooler pods pointing to the Postgres primary. + Optional. Overrides `enableMasterPoolerLoadBalancer`. + +* **enableReplicaNodePort** + boolean flag to override the operator defaults (set by the + `enable_replica_node_port` parameter) to define whether to enable the node + port pointing to the Postgres standby instances. Optional. Overrides `enableReplicaLoadBalancer`. + +* **enableReplicaPoolerNodePort** + boolean flag to override the operator defaults (set by the + `enable_replica_pooler_node_port` parameter) to define whether to enable + the node port for replica pooler pods pointing to the Postgres standby + instances. Optional. Overrides `enableReplicaPoolerLoadBalancer`. + +* **masterNodePort** + integer flag to specify a port number for the node port to the Postgres primary. + Only used when `enableMasterNodePort` or `enable_master_node_port` are enabled. + Optional. Kubernetes will provide a port number for you if not specified. + +* **masterPoolerNodePort** + integer flag to specify a port number for the node port for the master pooler pods pointing to the Postgres primary. + Only used when `enableMasterPoolerNodePort` or `enable_master_pooler_node_port` are enabled. + Optional. Kubernetes will provide a port number for you if not specified. + +* **replicaNodePort** + integer flag to specify a port number for the node port pointing to the Postgres standby instances. + Only used when `enableReplicaNodePort` or `enable_replica_node_port` are enabled. + Optional. Kubernetes will provide a port number for you if not specified. + +* **replicaPoolerNodePort** + integer flag to specify a port number for the node port for the replica pooler pods pointing to the Postgres standby instances + Only used when `enableReplicaPoolerNodePort` or `enable_replica_pooler_node_port` are enabled. + Optional. Kubernetes will provide a port number for you if not specified. + * **maintenanceWindows** a list which defines specific time frames when certain maintenance operations such as automatic major upgrades or master pod migration are allowed to happen. diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index b6b58f072..6b43d0c54 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -748,6 +748,18 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "enable_replica_pooler_load_balancer": { Type: "boolean", }, + "enable_master_node_port": { + Type: "boolean", + }, + "enable_master_pooler_node_port": { + Type: "boolean", + }, + "enable_replica_node_port": { + Type: "boolean", + }, + "enable_replica_pooler_node_port": { + Type: "boolean", + }, "external_traffic_policy": { Type: "string", Enum: []apiextv1.JSON{ 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 0087e5850..3f28effc8 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -137,17 +137,24 @@ type OperatorTimeouts struct { // LoadBalancerConfiguration defines the LB configuration type LoadBalancerConfiguration struct { - DbHostedZone string `json:"db_hosted_zone,omitempty"` - EnableMasterLoadBalancer bool `json:"enable_master_load_balancer,omitempty"` - EnableMasterPoolerLoadBalancer bool `json:"enable_master_pooler_load_balancer,omitempty"` - EnableReplicaLoadBalancer bool `json:"enable_replica_load_balancer,omitempty"` - EnableReplicaPoolerLoadBalancer bool `json:"enable_replica_pooler_load_balancer,omitempty"` - CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"` - MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"` - MasterLegacyDNSNameFormat config.StringTemplate `json:"master_legacy_dns_name_format,omitempty"` - ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"` - ReplicaLegacyDNSNameFormat config.StringTemplate `json:"replica_legacy_dns_name_format,omitempty"` - ExternalTrafficPolicy string `json:"external_traffic_policy" default:"Cluster"` + DbHostedZone string `json:"db_hosted_zone,omitempty"` + EnableMasterLoadBalancer bool `json:"enable_master_load_balancer,omitempty"` + EnableMasterPoolerLoadBalancer bool `json:"enable_master_pooler_load_balancer,omitempty"` + EnableReplicaLoadBalancer bool `json:"enable_replica_load_balancer,omitempty"` + EnableReplicaPoolerLoadBalancer bool `json:"enable_replica_pooler_load_balancer,omitempty"` + + // kept in LoadBalancerConfiguration because all the other parameters apply here too + EnableMasterNodePort bool `json:"enable_master_node_port,omitempty"` + EnableMasterPoolerNodePort bool `json:"enable_master_pooler_node_port,omitempty"` + EnableReplicaNodePort bool `json:"enable_replica_node_port,omitempty"` + EnableReplicaPoolerNodePort bool `json:"enable_replica_pooler_node_port,omitempty"` + + CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"` + MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"` + MasterLegacyDNSNameFormat config.StringTemplate `json:"master_legacy_dns_name_format,omitempty"` + ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"` + ReplicaLegacyDNSNameFormat config.StringTemplate `json:"replica_legacy_dns_name_format,omitempty"` + ExternalTrafficPolicy string `json:"external_traffic_policy" default:"Cluster"` } // AWSGCPConfiguration defines the configuration for AWS diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index d67f23741..71ac73133 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -63,6 +63,18 @@ type PostgresSpec struct { EnableReplicaLoadBalancer *bool `json:"enableReplicaLoadBalancer,omitempty"` EnableReplicaPoolerLoadBalancer *bool `json:"enableReplicaPoolerLoadBalancer,omitempty"` + // vars to enable and configure nodeport services + // set ports to 0 or nil to let kubernetes decide which port to use + // overrides loadbalancer configuration + EnableMasterNodePort *bool `json:"enableMasterNodePort,omitempty"` + MasterNodePort *int32 `json:"masterNodePort,omitempty"` + EnableMasterPoolerNodePort *bool `json:"enableMasterPoolerNodePort,omitempty"` + MasterPoolerNodePort *int32 `json:"masterPoolerNodePort,omitempty"` + EnableReplicaNodePort *bool `json:"enableReplicaNodePort,omitempty"` + ReplicaNodePort *int32 `json:"replicaNodePort,omitempty"` + EnableReplicaPoolerNodePort *bool `json:"enableReplicaPoolerNodePort,omitempty"` + ReplicaPoolerNodePort *int32 `json:"replicaPoolerNodePort,omitempty"` + // deprecated load balancer settings maintained for backward compatibility // see "Load balancers" operator docs UseLoadBalancer *bool `json:"useLoadBalancer,omitempty"` 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 3fdc31fa7..ff83abec9 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -735,6 +735,46 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { *out = new(bool) **out = **in } + if in.EnableMasterNodePort != nil { + in, out := &in.EnableMasterNodePort, &out.EnableMasterNodePort + *out = new(bool) + **out = **in + } + if in.MasterNodePort != nil { + in, out := &in.MasterNodePort, &out.MasterNodePort + *out = new(int32) + **out = **in + } + if in.EnableMasterPoolerNodePort != nil { + in, out := &in.EnableMasterPoolerNodePort, &out.EnableMasterPoolerNodePort + *out = new(bool) + **out = **in + } + if in.MasterPoolerNodePort != nil { + in, out := &in.MasterPoolerNodePort, &out.MasterPoolerNodePort + *out = new(int32) + **out = **in + } + if in.EnableReplicaNodePort != nil { + in, out := &in.EnableReplicaNodePort, &out.EnableReplicaNodePort + *out = new(bool) + **out = **in + } + if in.ReplicaNodePort != nil { + in, out := &in.ReplicaNodePort, &out.ReplicaNodePort + *out = new(int32) + **out = **in + } + if in.EnableReplicaPoolerNodePort != nil { + in, out := &in.EnableReplicaPoolerNodePort, &out.EnableReplicaPoolerNodePort + *out = new(bool) + **out = **in + } + if in.ReplicaPoolerNodePort != nil { + in, out := &in.ReplicaPoolerNodePort, &out.ReplicaPoolerNodePort + *out = new(int32) + **out = **in + } if in.UseLoadBalancer != nil { in, out := &in.UseLoadBalancer, &out.UseLoadBalancer *out = new(bool) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 95f16d922..6d9e6150a 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -859,6 +859,14 @@ func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) { return false, "new service's ExternalTrafficPolicy does not match the current one" } + if len(old.Spec.Ports) > 0 && len(new.Spec.Ports) > 0 { + // we need to check whether the new port is not zero (=user-defined) + // and only overwrite if it is + if new.Spec.Ports[0].NodePort != 0 && old.Spec.Ports[0].NodePort != new.Spec.Ports[0].NodePort { + return false, "new service's NodePort does not match the current one" + } + } + return true, "" } diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 84d75cb0a..5fdf1a220 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -1334,7 +1334,8 @@ func newService( svcType v1.ServiceType, sourceRanges []string, selector map[string]string, - policy v1.ServiceExternalTrafficPolicyType) *v1.Service { + policy v1.ServiceExternalTrafficPolicyType, + nodePort *int32) *v1.Service { svc := &v1.Service{ Spec: v1.ServiceSpec{ Selector: selector, @@ -1344,6 +1345,16 @@ func newService( }, } svc.Annotations = annotations + + if nodePort != nil { + svc.Spec.Ports = []v1.ServicePort{ + { + Name: "port", + NodePort: *nodePort, + }, + } + } + return svc } @@ -1370,6 +1381,7 @@ func TestCompareServices(t *testing.T) { []string{"128.141.0.0/16", "137.138.0.0/16"}, nil, defaultPolicy, + nil, ) ownerRef := metav1.OwnerReference{ @@ -1381,6 +1393,9 @@ func TestCompareServices(t *testing.T) { serviceWithOwnerReference.ObjectMeta.OwnerReferences = append(serviceWithOwnerReference.ObjectMeta.OwnerReferences, ownerRef) + portZero := int32(0) + portNotZero := int32(1337) + tests := []struct { about string current *v1.Service @@ -1396,14 +1411,14 @@ func TestCompareServices(t *testing.T) { }, v1.ServiceTypeClusterIP, []string{"128.141.0.0/16", "137.138.0.0/16"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), new: newService( map[string]string{ constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", }, v1.ServiceTypeClusterIP, []string{"128.141.0.0/16", "137.138.0.0/16"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), match: true, }, { @@ -1414,14 +1429,14 @@ func TestCompareServices(t *testing.T) { }, v1.ServiceTypeClusterIP, []string{"128.141.0.0/16", "137.138.0.0/16"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), new: newService( map[string]string{ constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", }, v1.ServiceTypeLoadBalancer, []string{"128.141.0.0/16", "137.138.0.0/16"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), match: false, reason: `new service's type "LoadBalancer" does not match the current one "ClusterIP"`, }, @@ -1433,14 +1448,14 @@ func TestCompareServices(t *testing.T) { }, v1.ServiceTypeLoadBalancer, []string{"128.141.0.0/16", "137.138.0.0/16"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), new: newService( map[string]string{ constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", }, v1.ServiceTypeLoadBalancer, []string{"185.249.56.0/22"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), match: false, reason: `new service's LoadBalancerSourceRange does not match the current one`, }, @@ -1452,14 +1467,14 @@ func TestCompareServices(t *testing.T) { }, v1.ServiceTypeLoadBalancer, []string{"128.141.0.0/16", "137.138.0.0/16"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), new: newService( map[string]string{ constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", }, v1.ServiceTypeLoadBalancer, []string{}, - nil, defaultPolicy), + nil, defaultPolicy, nil), match: false, reason: `new service's LoadBalancerSourceRange does not match the current one`, }, @@ -1471,7 +1486,7 @@ func TestCompareServices(t *testing.T) { }, v1.ServiceTypeClusterIP, []string{"128.141.0.0/16", "137.138.0.0/16"}, - nil, defaultPolicy), + nil, defaultPolicy, nil), new: serviceWithOwnerReference, match: false, }, @@ -1481,12 +1496,12 @@ func TestCompareServices(t *testing.T) { map[string]string{}, v1.ServiceTypeClusterIP, []string{}, - nil, defaultPolicy), + nil, defaultPolicy, nil), new: newService( map[string]string{}, v1.ServiceTypeClusterIP, []string{}, - map[string]string{"cluster-name": "clstr", "spilo-role": "master"}, defaultPolicy), + map[string]string{"cluster-name": "clstr", "spilo-role": "master"}, defaultPolicy, nil), match: false, }, { @@ -1495,14 +1510,42 @@ func TestCompareServices(t *testing.T) { map[string]string{}, v1.ServiceTypeClusterIP, []string{}, - nil, defaultPolicy), + nil, defaultPolicy, nil), new: newService( map[string]string{}, v1.ServiceTypeClusterIP, []string{}, - nil, v1.ServiceExternalTrafficPolicyTypeLocal), + nil, v1.ServiceExternalTrafficPolicyTypeLocal, nil), match: false, }, + { + about: "services differ on node port", + current: newService( + map[string]string{}, + v1.ServiceTypeNodePort, + []string{}, + nil, defaultPolicy, &portZero), + new: newService( + map[string]string{}, + v1.ServiceTypeNodePort, + []string{}, + nil, defaultPolicy, &portNotZero), + match: false, + }, + { + about: "services do not differ on node port when requesting 0", + current: newService( + map[string]string{}, + v1.ServiceTypeNodePort, + []string{}, + nil, defaultPolicy, &portNotZero), + new: newService( + map[string]string{}, + v1.ServiceTypeNodePort, + []string{}, + nil, defaultPolicy, &portZero), + match: true, + }, } for _, tt := range tests { diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 9f071068c..85685774d 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -566,7 +566,9 @@ func (c *Cluster) generateConnectionPoolerService(connectionPooler *ConnectionPo }, } - if c.shouldCreateLoadBalancerForPoolerService(poolerRole, spec) { + if ok, port := c.shouldCreateNodePortForPoolerService(poolerRole, spec); ok { + c.configureNodePortService(&serviceSpec, port) + } else if c.shouldCreateLoadBalancerForPoolerService(poolerRole, spec) { c.configureLoadBalanceService(&serviceSpec, spec.AllowedSourceRanges) } @@ -594,7 +596,9 @@ func (c *Cluster) generatePoolerServiceAnnotations(role PostgresRole, spec *acid var dnsString string annotations := c.getCustomServiceAnnotations(role, spec) - if c.shouldCreateLoadBalancerForPoolerService(role, spec) { + nodePort, _ := c.shouldCreateNodePortForPoolerService(role, spec) + + if !nodePort && c.shouldCreateLoadBalancerForPoolerService(role, spec) { // -repl suffix will be added by replicaDNSName clusterNameWithPoolerSuffix := c.connectionPoolerName(Master) if role == Master { @@ -635,6 +639,37 @@ func (c *Cluster) shouldCreateLoadBalancerForPoolerService(role PostgresRole, sp } } +func (c *Cluster) shouldCreateNodePortForPoolerService(role PostgresRole, spec *acidv1.PostgresSpec) (bool, int32) { + switch role { + case Replica: + // if the value is explicitly set in a Postgresql manifest, follow this setting + if spec.EnableReplicaPoolerNodePort != nil { + port := int32(0) + if spec.ReplicaPoolerNodePort != nil { + port = *spec.ReplicaPoolerNodePort + } + + return *spec.EnableReplicaPoolerNodePort, port + } + + // otherwise, follow the operator configuration + return c.OpConfig.EnableReplicaPoolerNodePort, 0 + case Master: + if spec.EnableMasterPoolerNodePort != nil { + port := int32(0) + if spec.MasterPoolerNodePort != nil { + port = *spec.MasterPoolerNodePort + } + + return *spec.EnableMasterPoolerNodePort, port + } + + return c.OpConfig.EnableMasterPoolerNodePort, 0 + default: + panic(fmt.Sprintf("Unknown role %v", role)) + } +} + func (c *Cluster) listPoolerPods(listOptions metav1.ListOptions) ([]v1.Pod, error) { pods, err := c.KubeClient.Pods(c.Namespace).List(context.TODO(), listOptions) if err != nil { diff --git a/pkg/cluster/connection_pooler_test.go b/pkg/cluster/connection_pooler_test.go index 23213520f..1b41cbb02 100644 --- a/pkg/cluster/connection_pooler_test.go +++ b/pkg/cluster/connection_pooler_test.go @@ -1154,3 +1154,181 @@ func TestConnectionPoolerServiceSpec(t *testing.T) { } } } + +func TestConnectionPoolerServiceType(t *testing.T) { + testName := "Test connection pooler service type selection" + + cluster := New( + Config{ + OpConfig: config.Config{ + ProtectedRoles: []string{"admin"}, + Auth: config.Auth{ + SuperUsername: superUserName, + ReplicationUsername: replicationUserName, + }, + ConnectionPooler: config.ConnectionPooler{ + ConnectionPoolerDefaultCPURequest: "100m", + ConnectionPoolerDefaultCPULimit: "100m", + ConnectionPoolerDefaultMemoryRequest: "100Mi", + ConnectionPoolerDefaultMemoryLimit: "100Mi", + }, + Resources: config.Resources{ + EnableOwnerReferences: util.True(), + }, + }, + }, + k8sutil.KubernetesClient{}, + acidv1.Postgresql{}, + logger, + eventRecorder, + ) + + cluster.Statefulset = &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sts", + }, + } + + cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{ + Master: { + Deployment: nil, + Service: nil, + LookupFunction: false, + Role: Master, + }, + Replica: { + Deployment: nil, + Service: nil, + LookupFunction: false, + Role: Replica, + }, + } + + tests := []struct { + subTest string + spec *acidv1.PostgresSpec + cluster *Cluster + expectedType map[PostgresRole]v1.ServiceType + }{ + { + subTest: "default configuration -> ClusterIP for both", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeClusterIP, + Replica: v1.ServiceTypeClusterIP, + }, + }, + { + subTest: "LoadBalancer for both roles", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + EnableMasterPoolerLoadBalancer: boolToPointer(true), + EnableReplicaPoolerLoadBalancer: boolToPointer(true), + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeLoadBalancer, + Replica: v1.ServiceTypeLoadBalancer, + }, + }, + { + subTest: "LoadBalancer for master", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + EnableMasterPoolerLoadBalancer: boolToPointer(true), + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeLoadBalancer, + Replica: v1.ServiceTypeClusterIP, + }, + }, + { + subTest: "LoadBalancer for replica", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + EnableReplicaPoolerLoadBalancer: boolToPointer(true), + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeClusterIP, + Replica: v1.ServiceTypeLoadBalancer, + }, + }, + { + subTest: "NodePort for both roles", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + EnableMasterPoolerNodePort: boolToPointer(true), + EnableReplicaPoolerNodePort: boolToPointer(true), + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeNodePort, + Replica: v1.ServiceTypeNodePort, + }, + }, + { + subTest: "NodePort for master", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + EnableMasterPoolerNodePort: boolToPointer(true), + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeNodePort, + Replica: v1.ServiceTypeClusterIP, + }, + }, + { + subTest: "NodePort for replica", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + EnableReplicaPoolerNodePort: boolToPointer(true), + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeClusterIP, + Replica: v1.ServiceTypeNodePort, + }, + }, + { + subTest: "NodePort overrides LoadBalancer for both roles", + spec: &acidv1.PostgresSpec{ + ConnectionPooler: &acidv1.ConnectionPooler{}, + EnableMasterPoolerLoadBalancer: boolToPointer(true), + EnableReplicaPoolerLoadBalancer: boolToPointer(true), + EnableMasterPoolerNodePort: boolToPointer(true), + EnableReplicaPoolerNodePort: boolToPointer(true), + }, + cluster: cluster, + expectedType: map[PostgresRole]v1.ServiceType{ + Master: v1.ServiceTypeNodePort, + Replica: v1.ServiceTypeNodePort, + }, + }, + } + + roles := []PostgresRole{Master, Replica} + + for _, tt := range tests { + tt.cluster.Spec = *tt.spec + + for _, role := range roles { + svc := tt.cluster.generateConnectionPoolerService(tt.cluster.ConnectionPooler[role]) + + expected, ok := tt.expectedType[role] + if !ok { + t.Fatalf("%s [%s]: missing expectedType for role %v", testName, tt.subTest, role) + } + + if svc.Spec.Type != expected { + t.Errorf("%s [%s] role=%s: service Type is incorrect, got %s, expected %s", + testName, tt.subTest, role, svc.Spec.Type, expected) + } + } + } +} diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 8d3a40d9a..302797dc4 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -2004,6 +2004,37 @@ func (c *Cluster) shouldCreateLoadBalancerForService(role PostgresRole, spec *ac } +func (c *Cluster) shouldCreateNodePortForService(role PostgresRole, spec *acidv1.PostgresSpec) (bool, int32) { + switch role { + case Replica: + // if the value is explicitly set in a Postgresql manifest, follow this setting + if spec.EnableReplicaNodePort != nil { + port := int32(0) + if spec.ReplicaNodePort != nil { + port = *spec.ReplicaNodePort + } + + return *spec.EnableReplicaNodePort, port + } + + // otherwise, follow the operator configuration + return c.OpConfig.EnableReplicaNodePort, 0 + case Master: + if spec.EnableMasterNodePort != nil { + port := int32(0) + if spec.MasterNodePort != nil { + port = *spec.MasterNodePort + } + + return *spec.EnableMasterNodePort, port + } + + return c.OpConfig.EnableMasterNodePort, 0 + default: + panic(fmt.Sprintf("Unknown role %v", role)) + } +} + func (c *Cluster) generateService(role PostgresRole, spec *acidv1.PostgresSpec) *v1.Service { serviceSpec := v1.ServiceSpec{ Ports: []v1.ServicePort{{Name: "postgresql", Port: pgPort, TargetPort: intstr.IntOrString{IntVal: pgPort}}}, @@ -2016,7 +2047,9 @@ func (c *Cluster) generateService(role PostgresRole, spec *acidv1.PostgresSpec) serviceSpec.Selector = c.roleLabelsSet(false, role) } - if c.shouldCreateLoadBalancerForService(role, spec) { + if ok, port := c.shouldCreateNodePortForService(role, spec); ok { + c.configureNodePortService(&serviceSpec, port) + } else if c.shouldCreateLoadBalancerForService(role, spec) { c.configureLoadBalanceService(&serviceSpec, spec.AllowedSourceRanges) } @@ -2049,10 +2082,21 @@ func (c *Cluster) configureLoadBalanceService(serviceSpec *v1.ServiceSpec, sourc serviceSpec.Type = v1.ServiceTypeLoadBalancer } +func (c *Cluster) configureNodePortService(serviceSpec *v1.ServiceSpec, port int32) { + serviceSpec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyType(c.OpConfig.ExternalTrafficPolicy) + serviceSpec.Type = v1.ServiceTypeNodePort + + if port != 0 && len(serviceSpec.Ports) > 0 { + serviceSpec.Ports[0].NodePort = port + } +} + func (c *Cluster) generateServiceAnnotations(role PostgresRole, spec *acidv1.PostgresSpec) map[string]string { annotations := c.getCustomServiceAnnotations(role, spec) - if c.shouldCreateLoadBalancerForService(role, spec) { + nodePort, _ := c.shouldCreateNodePortForService(role, spec) + + if !nodePort && c.shouldCreateLoadBalancerForService(role, spec) { dnsName := c.dnsName(role) // External DNS name annotation is not customizable diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index ef408da4d..bf21b8645 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -2971,32 +2971,32 @@ func newLBFakeClient() (k8sutil.KubernetesClient, *fake.Clientset) { }, clientSet } -func getServices(serviceType v1.ServiceType, sourceRanges []string, extTrafficPolicy, clusterName string) []v1.ServiceSpec { +func getServices(serviceType v1.ServiceType, sourceRanges []string, extTrafficPolicy, clusterName string, nodePort int32) []v1.ServiceSpec { return []v1.ServiceSpec{ { ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy), LoadBalancerSourceRanges: sourceRanges, - Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}}, + Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}}, Type: serviceType, }, { ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy), LoadBalancerSourceRanges: sourceRanges, - Ports: []v1.ServicePort{{Name: clusterName + "-pooler", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}}, + Ports: []v1.ServicePort{{Name: clusterName + "-pooler", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}}, Selector: map[string]string{"connection-pooler": clusterName + "-pooler"}, Type: serviceType, }, { ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy), LoadBalancerSourceRanges: sourceRanges, - Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}}, + Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}}, Selector: map[string]string{"spilo-role": "replica", "application": "spilo", "cluster-name": clusterName}, Type: serviceType, }, { ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy), LoadBalancerSourceRanges: sourceRanges, - Ports: []v1.ServicePort{{Name: clusterName + "-pooler-repl", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}}, + Ports: []v1.ServicePort{{Name: clusterName + "-pooler-repl", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}}, Selector: map[string]string{"connection-pooler": clusterName + "-pooler-repl"}, Type: serviceType, }, @@ -3064,7 +3064,7 @@ func TestEnableLoadBalancers(t *testing.T) { }, }, }, - expectedServices: getServices(v1.ServiceTypeClusterIP, nil, "", clusterName), + expectedServices: getServices(v1.ServiceTypeClusterIP, nil, "", clusterName, 0), }, { subTest: "LBs enabled in manifest, disabled in config", @@ -3111,7 +3111,7 @@ func TestEnableLoadBalancers(t *testing.T) { }, }, }, - expectedServices: getServices(v1.ServiceTypeLoadBalancer, sourceRanges, extTrafficPolicy, clusterName), + expectedServices: getServices(v1.ServiceTypeLoadBalancer, sourceRanges, extTrafficPolicy, clusterName, 0), }, } @@ -3143,6 +3143,195 @@ func TestEnableLoadBalancers(t *testing.T) { } } +func TestEnableNodePorts(t *testing.T) { + clusterName := "acid-test-cluster" + namespace := "default" + clusterNameLabel := "cluster-name" + roleLabel := "spilo-role" + roles := []PostgresRole{Master, Replica} + extTrafficPolicy := "Cluster" + port := int32(1337) + + tests := []struct { + subTest string + config config.Config + pgSpec acidv1.Postgresql + expectedServices []v1.ServiceSpec + }{ + { + subTest: "NodePorts enabled in config, disabled in manifest", + config: config.Config{ + ConnectionPooler: config.ConnectionPooler{ + ConnectionPoolerDefaultCPURequest: "100m", + ConnectionPoolerDefaultCPULimit: "100m", + ConnectionPoolerDefaultMemoryRequest: "100Mi", + ConnectionPoolerDefaultMemoryLimit: "100Mi", + NumberOfInstances: k8sutil.Int32ToPointer(1), + }, + EnableMasterNodePort: true, + EnableMasterPoolerNodePort: true, + EnableReplicaNodePort: true, + EnableReplicaPoolerNodePort: true, + ExternalTrafficPolicy: extTrafficPolicy, + Resources: config.Resources{ + ClusterLabels: map[string]string{"application": "spilo"}, + ClusterNameLabel: clusterNameLabel, + PodRoleLabel: roleLabel, + }, + }, + pgSpec: acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: namespace, + }, + Spec: acidv1.PostgresSpec{ + EnableConnectionPooler: util.True(), + EnableReplicaConnectionPooler: util.True(), + EnableMasterNodePort: util.False(), + EnableMasterPoolerNodePort: util.False(), + EnableReplicaNodePort: util.False(), + EnableReplicaPoolerNodePort: util.False(), + NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + TeamID: "acid", + Volume: acidv1.Volume{ + Size: "1G", + }, + }, + }, + expectedServices: getServices(v1.ServiceTypeClusterIP, nil, "", clusterName, 0), + }, + { + subTest: "NodePorts configured in manifest, disabled in config", + config: config.Config{ + ConnectionPooler: config.ConnectionPooler{ + ConnectionPoolerDefaultCPURequest: "100m", + ConnectionPoolerDefaultCPULimit: "100m", + ConnectionPoolerDefaultMemoryRequest: "100Mi", + ConnectionPoolerDefaultMemoryLimit: "100Mi", + NumberOfInstances: k8sutil.Int32ToPointer(1), + }, + EnableMasterNodePort: false, + EnableMasterPoolerNodePort: false, + EnableReplicaNodePort: false, + EnableReplicaPoolerNodePort: false, + ExternalTrafficPolicy: extTrafficPolicy, + Resources: config.Resources{ + ClusterLabels: map[string]string{"application": "spilo"}, + ClusterNameLabel: clusterNameLabel, + PodRoleLabel: roleLabel, + }, + }, + pgSpec: acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: namespace, + }, + Spec: acidv1.PostgresSpec{ + EnableConnectionPooler: util.True(), + EnableReplicaConnectionPooler: util.True(), + EnableMasterNodePort: util.True(), + EnableMasterPoolerNodePort: util.True(), + EnableReplicaNodePort: util.True(), + EnableReplicaPoolerNodePort: util.True(), + NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + TeamID: "acid", + Volume: acidv1.Volume{ + Size: "1G", + }, + }, + }, + expectedServices: getServices(v1.ServiceTypeNodePort, nil, extTrafficPolicy, clusterName, 0), + }, + { + subTest: "NodePorts configured in manifest, disabled in config, custom port specified", + config: config.Config{ + ConnectionPooler: config.ConnectionPooler{ + ConnectionPoolerDefaultCPURequest: "100m", + ConnectionPoolerDefaultCPULimit: "100m", + ConnectionPoolerDefaultMemoryRequest: "100Mi", + ConnectionPoolerDefaultMemoryLimit: "100Mi", + NumberOfInstances: k8sutil.Int32ToPointer(1), + }, + EnableMasterNodePort: false, + EnableMasterPoolerNodePort: false, + EnableReplicaNodePort: false, + EnableReplicaPoolerNodePort: false, + ExternalTrafficPolicy: extTrafficPolicy, + Resources: config.Resources{ + ClusterLabels: map[string]string{"application": "spilo"}, + ClusterNameLabel: clusterNameLabel, + PodRoleLabel: roleLabel, + }, + }, + pgSpec: acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: namespace, + }, + Spec: acidv1.PostgresSpec{ + EnableConnectionPooler: util.True(), + EnableReplicaConnectionPooler: util.True(), + EnableMasterNodePort: util.True(), + MasterNodePort: &port, + EnableMasterPoolerNodePort: util.True(), + MasterPoolerNodePort: &port, + EnableReplicaNodePort: util.True(), + ReplicaNodePort: &port, + EnableReplicaPoolerNodePort: util.True(), + ReplicaPoolerNodePort: &port, + NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + TeamID: "acid", + Volume: acidv1.Volume{ + Size: "1G", + }, + }, + }, + expectedServices: getServices(v1.ServiceTypeNodePort, nil, extTrafficPolicy, clusterName, port), + }, + } + + for _, tt := range tests { + client, _ := newLBFakeClient() + + var cluster = New( + Config{ + OpConfig: tt.config, + }, client, tt.pgSpec, logger, eventRecorder) + + cluster.Name = clusterName + cluster.Namespace = namespace + cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{} + generatedServices := make([]v1.ServiceSpec, 0) + for _, role := range roles { + cluster.syncService(role) + cluster.ConnectionPooler[role] = &ConnectionPoolerObjects{ + Name: cluster.connectionPoolerName(role), + ClusterName: cluster.Name, + Namespace: cluster.Namespace, + Role: role, + } + cluster.syncConnectionPoolerWorker(&tt.pgSpec, &tt.pgSpec, role) + generatedServices = append(generatedServices, cluster.Services[role].Spec) + generatedServices = append(generatedServices, cluster.ConnectionPooler[role].Service.Spec) + } + if !reflect.DeepEqual(tt.expectedServices, generatedServices) { + t.Errorf("%s %s: expected %#v but got %#v", t.Name(), tt.subTest, tt.expectedServices, generatedServices) + } + } +} + func TestGenerateResourceRequirements(t *testing.T) { client, _ := newFakeK8sTestClient() clusterName := "acid-test-cluster" diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index ed3eb3d75..1fdad3e5e 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -324,9 +324,14 @@ func (c *Cluster) updateService(role PostgresRole, oldService *v1.Service, newSe // 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 { + if newServiceType != oldServiceType && oldServiceType == v1.ServiceTypeLoadBalancer { + // Kubernetes rejects updates that change type away from LoadBalancer while + // loadBalancerSourceRanges is still set; clear it before updating + newService.Spec.LoadBalancerSourceRanges = nil newService.ResourceVersion = oldService.ResourceVersion - newService.Spec.ClusterIP = oldService.Spec.ClusterIP + if newServiceType == v1.ServiceTypeClusterIP { + newService.Spec.ClusterIP = oldService.Spec.ClusterIP + } } svc, err = c.KubeClient.Services(serviceName.Namespace).Update(context.TODO(), newService, metav1.UpdateOptions{}) if err != nil { diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index e304c14a5..9d752a76e 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -171,6 +171,10 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.EnableMasterPoolerLoadBalancer = fromCRD.LoadBalancer.EnableMasterPoolerLoadBalancer result.EnableReplicaLoadBalancer = fromCRD.LoadBalancer.EnableReplicaLoadBalancer result.EnableReplicaPoolerLoadBalancer = fromCRD.LoadBalancer.EnableReplicaPoolerLoadBalancer + result.EnableMasterNodePort = fromCRD.LoadBalancer.EnableMasterNodePort + result.EnableMasterPoolerNodePort = fromCRD.LoadBalancer.EnableMasterPoolerNodePort + result.EnableReplicaNodePort = fromCRD.LoadBalancer.EnableReplicaNodePort + result.EnableReplicaPoolerNodePort = fromCRD.LoadBalancer.EnableReplicaPoolerNodePort result.CustomServiceAnnotations = fromCRD.LoadBalancer.CustomServiceAnnotations result.MasterDNSNameFormat = fromCRD.LoadBalancer.MasterDNSNameFormat result.MasterLegacyDNSNameFormat = fromCRD.LoadBalancer.MasterLegacyDNSNameFormat diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index a14022407..9a18e0d25 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -213,6 +213,10 @@ type Config struct { EnableMasterPoolerLoadBalancer bool `name:"enable_master_pooler_load_balancer" default:"false"` EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"` EnableReplicaPoolerLoadBalancer bool `name:"enable_replica_pooler_load_balancer" default:"false"` + EnableMasterNodePort bool `name:"enable_master_node_port" default:"false"` + EnableMasterPoolerNodePort bool `name:"enable_master_pooler_node_port" default:"false"` + EnableReplicaNodePort bool `name:"enable_replica_node_port" default:"false"` + EnableReplicaPoolerNodePort bool `name:"enable_replica_pooler_node_port" default:"false"` CustomServiceAnnotations map[string]string `name:"custom_service_annotations"` CustomPodAnnotations map[string]string `name:"custom_pod_annotations"` EnablePodAntiAffinity bool `name:"enable_pod_antiaffinity" default:"false"` From ebf48667f10fb023c9f1eadb7a698e7149b8aef3 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Fri, 12 Jun 2026 10:42:37 +0200 Subject: [PATCH 28/70] drop kubectl-pg plugin (#3107) Co-authored-by: Ida Novindasari --- .gitignore | 1 - kubectl-pg/README.md | 137 ------------------------ kubectl-pg/build.sh | 4 - kubectl-pg/cmd/addDb.go | 114 -------------------- kubectl-pg/cmd/addUser.go | 144 ------------------------- kubectl-pg/cmd/check.go | 74 ------------- kubectl-pg/cmd/connect.go | 144 ------------------------- kubectl-pg/cmd/create.go | 82 -------------- kubectl-pg/cmd/delete.go | 134 ----------------------- kubectl-pg/cmd/extVolume.go | 119 --------------------- kubectl-pg/cmd/list.go | 125 ---------------------- kubectl-pg/cmd/logs.go | 143 ------------------------- kubectl-pg/cmd/root.go | 51 --------- kubectl-pg/cmd/scale.go | 194 --------------------------------- kubectl-pg/cmd/update.go | 96 ----------------- kubectl-pg/cmd/util.go | 172 ------------------------------ kubectl-pg/cmd/version.go | 80 -------------- kubectl-pg/go.mod | 72 ------------- kubectl-pg/go.sum | 206 ------------------------------------ kubectl-pg/main.go | 31 ------ 20 files changed, 2123 deletions(-) delete mode 100644 kubectl-pg/README.md delete mode 100755 kubectl-pg/build.sh delete mode 100644 kubectl-pg/cmd/addDb.go delete mode 100644 kubectl-pg/cmd/addUser.go delete mode 100644 kubectl-pg/cmd/check.go delete mode 100644 kubectl-pg/cmd/connect.go delete mode 100644 kubectl-pg/cmd/create.go delete mode 100644 kubectl-pg/cmd/delete.go delete mode 100644 kubectl-pg/cmd/extVolume.go delete mode 100644 kubectl-pg/cmd/list.go delete mode 100644 kubectl-pg/cmd/logs.go delete mode 100644 kubectl-pg/cmd/root.go delete mode 100644 kubectl-pg/cmd/scale.go delete mode 100644 kubectl-pg/cmd/update.go delete mode 100644 kubectl-pg/cmd/util.go delete mode 100644 kubectl-pg/cmd/version.go delete mode 100644 kubectl-pg/go.mod delete mode 100644 kubectl-pg/go.sum delete mode 100644 kubectl-pg/main.go diff --git a/.gitignore b/.gitignore index 5938db216..65aad49c5 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ _testmain.go *.test *.prof /vendor/ -/kubectl-pg/vendor/ /build/ /docker/build/ /github.com/ diff --git a/kubectl-pg/README.md b/kubectl-pg/README.md deleted file mode 100644 index 8213d4ff5..000000000 --- a/kubectl-pg/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# Kubectl Plugin for Zalando's Postgres Operator - -This plugin is a prototype developed as a part of **Google Summer of Code 2019** under the [Postgres Operator](https://summerofcode.withgoogle.com/archive/2019/organizations/6187982082539520/) organization. - -## Installation of kubectl pg plugin - -This project uses Go Modules for dependency management to build locally. -Install go and enable go modules with ```export GO111MODULE=on```. -From Go >=1.13 Go modules will be enabled by default. - -```bash -# Assumes you have a working KUBECONFIG -$ GO111MODULE="on" -$ GOPATH/src/github.com/zalando/postgres-operator/kubectl-pg go mod vendor -# This generate a vendor directory with all dependencies needed by the plugin. -$ $GOPATH/src/github.com/zalando/postgres-operator/kubectl-pg go install -# This will place the kubectl-pg binary in your $GOPATH/bin -``` - -## Before using the kubectl pg plugin make sure to set KUBECONFIG env variable - -Ideally KUBECONFIG is found in $HOME/.kube/config else specify the KUBECONFIG path here. -```export KUBECONFIG=$HOME/.kube/config``` - -## List all commands available in kubectl pg - -```kubectl pg --help``` (or) ```kubectl pg``` - -## Check if Postgres Operator is installed and running - -```kubectl pg check``` - -## Create a new cluster using manifest file - -```kubectl pg create -f acid-minimal-cluster.yaml``` - -## List postgres resources - -```kubectl pg list``` - -List clusters across namespaces -```kubectl pg list all``` - -## Update existing cluster using manifest file - -```kubectl pg update -f acid-minimal-cluster.yaml``` - -## Delete existing cluster - -Using the manifest file: -```kubectl pg delete -f acid-minimal-cluster.yaml``` - -Or by specifying the cluster name: -```kubectl pg delete acid-minimal-cluster``` - -Use `--namespace` or `-n` flag if your cluster is in a different namespace to where your current context is pointing to: -```kubectl pg delete acid-minimal-cluster -n namespace01``` - -## Adding manifest roles to an existing cluster - -```kubectl pg add-user USER01 -p CREATEDB,LOGIN -c acid-minimal-cluster``` - -Privileges can only be [SUPERUSER, REPLICATION, INHERIT, LOGIN, NOLOGIN, CREATEROLE, CREATEDB, BYPASSRLS] -Note: By default, a LOGIN user is created (unless NOLOGIN is specified). - -## Adding databases to an existing cluster - -You have to specify an owner of the new database and this role must already exist in the cluster: -```kubectl pg add-db DB01 -o OWNER01 -c acid-minimal-cluster``` - -## Extend the volume of an existing pg cluster - -```kubectl pg ext-volume 2Gi -c acid-minimal-cluster``` - -## Print the version of Postgres Operator and kubectl pg plugin - -```kubectl pg version``` - -## Connect to the shell of a postgres pod - -Connect to the master pod: -```kubectl pg connect -c CLUSTER -m``` - -Connect to a random replica pod: -```kubectl pg connect -c CLUSTER``` - -Connect to a certain replica pod: -```kubectl pg connect -c CLUSTER -r 0``` - -## Connect to a database via psql - -Adding the `-p` flag allows you to directly connect to a given database with the psql client. -With `-u` you specify the user. If left out the name of the current OS user is taken. -`-d` lets you specify the database. If no database is specified, it will be the same as the user name. - -Connect to `app_db` database on the master with role `app_user`: -```kubectl pg connect -c CLUSTER -m -p -u app_user -d app_db``` - -Connect to the `postgres` database on a random replica with role `postgres`: -```kubectl pg connect -c CLUSTER -p -u postgres``` - -Connect to a certain replica assuming name of OS user, database role and name are all the same: -```kubectl pg connect -c CLUSTER -r 0 -p``` - - -## Access Postgres Operator logs - -```kubectl pg logs -o``` - -## Access Patroni logs of different database pods - -Fetch logs of master: -```kubectl pg logs -c CLUSTER -m``` - -Fetch logs of a random replica pod: -```kubectl pg logs -c CLUSTER``` - -Fetch logs of specified replica -```kubectl pg logs -c CLUSTER -r 2``` - -## Development - -When making changes to the plugin make sure to change the (major/patch) version of plugin in `build.sh` script and run `./build.sh`. - -## Google Summer of Code 2019 - -### GSoC Proposal - -[kubectl pg proposal](https://docs.google.com/document/d/1-WMy9HkfZ1XnnMbzplMe9rCzKrRMGaMz4owLVXXPb7w/edit) - -### Weekly Reports - -https://github.com/VineethReddy02/GSoC-Kubectl-Plugin-for-Postgres-Operator-tracker - -### Final Project Report - -https://gist.github.com/VineethReddy02/159283bd368a710379eaf0f6bd60a40a diff --git a/kubectl-pg/build.sh b/kubectl-pg/build.sh deleted file mode 100755 index a81bf54fc..000000000 --- a/kubectl-pg/build.sh +++ /dev/null @@ -1,4 +0,0 @@ - -VERSION=1.0 -sed -i "s/KubectlPgVersion string = \"[^\"]*\"/KubectlPgVersion string = \"${VERSION}\"/" cmd/version.go -go install \ No newline at end of file diff --git a/kubectl-pg/cmd/addDb.go b/kubectl-pg/cmd/addDb.go deleted file mode 100644 index 1c33579d9..000000000 --- a/kubectl-pg/cmd/addDb.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "log" - - "github.com/spf13/cobra" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" -) - -// addDbCmd represents the addDb command -var addDbCmd = &cobra.Command{ - Use: "add-db", - Short: "Adds a DB and its owner to a Postgres cluster. The owner role is created if it does not exist", - Long: `Adds a new DB to the Postgres cluster. Owner needs to be specified by the -o flag, cluster with -c flag.`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) > 0 { - dbName := args[0] - dbOwner, _ := cmd.Flags().GetString("owner") - clusterName, _ := cmd.Flags().GetString("cluster") - addDb(dbName, dbOwner, clusterName) - } else { - fmt.Println("database name can't be empty. Use kubectl pg add-db [-h | --help] for more info") - } - - }, - Example: ` -kubectl pg add-db db01 -o owner01 -c cluster01 -`, -} - -// add db and it's owner to the cluster -func addDb(dbName string, dbOwner string, clusterName string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - namespace := getCurrentNamespace() - postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - var dbOwnerExists bool - dbUsers := postgresql.Spec.Users - for key := range dbUsers { - if key == dbOwner { - dbOwnerExists = true - } - } - var patch []byte - // validating reserved DB names - if dbOwnerExists && dbName != "postgres" && dbName != "template0" && dbName != "template1" { - patch = dbPatch(dbName, dbOwner) - } else if !dbOwnerExists { - log.Fatal("The provided db-owner doesn't exist") - } else { - log.Fatal("The provided db-name is reserved by postgres") - } - - updatedPostgres, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patch, metav1.PatchOptions{}) - if err != nil { - log.Fatal(err) - } - - if updatedPostgres.ResourceVersion != postgresql.ResourceVersion { - fmt.Printf("Created new database %s with owner %s in PostgreSQL cluster %s.\n", dbName, dbOwner, updatedPostgres.Name) - } else { - fmt.Printf("postgresql %s is unchanged.\n", updatedPostgres.Name) - } -} - -func dbPatch(dbname string, owner string) []byte { - ins := map[string]map[string]map[string]string{"spec": {"databases": {dbname: owner}}} - patchInstances, err := json.Marshal(ins) - if err != nil { - log.Fatal(err, "unable to parse patch for add-db") - } - return patchInstances -} - -func init() { - addDbCmd.Flags().StringP("owner", "o", "", "provide owner of the database.") - addDbCmd.Flags().StringP("cluster", "c", "", "provide a postgres cluster name.") - rootCmd.AddCommand(addDbCmd) -} diff --git a/kubectl-pg/cmd/addUser.go b/kubectl-pg/cmd/addUser.go deleted file mode 100644 index 602adb51d..000000000 --- a/kubectl-pg/cmd/addUser.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "log" - "strings" - - "github.com/spf13/cobra" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" -) - -var allowedPrivileges = []string{"SUPERUSER", "REPLICATION", "INHERIT", "LOGIN", "NOLOGIN", "CREATEROLE", "CREATEDB", "BYPASSRLS"} - -// addUserCmd represents the addUser command -var addUserCmd = &cobra.Command{ - Use: "add-user", - Short: "Adds a user to the postgres cluster with given privileges", - Long: `Adds a user to the postgres cluster. You can add privileges as well with -p flag.`, - Run: func(cmd *cobra.Command, args []string) { - clusterName, _ := cmd.Flags().GetString("cluster") - privileges, _ := cmd.Flags().GetString("privileges") - - if len(args) > 0 { - user := args[0] - var permissions []string - var perms []string - - if privileges != "" { - parsedRoles := strings.Replace(privileges, ",", " ", -1) - parsedRoles = strings.ToUpper(parsedRoles) - permissions = strings.Fields(parsedRoles) - var invalidPerms []string - - for _, userPrivilege := range permissions { - validPerm := false - for _, privilege := range allowedPrivileges { - if privilege == userPrivilege { - perms = append(perms, userPrivilege) - validPerm = true - } - } - if !validPerm { - invalidPerms = append(invalidPerms, userPrivilege) - } - } - - if len(invalidPerms) > 0 { - fmt.Printf("Invalid privileges %s\n", invalidPerms) - return - } - } - addUser(user, clusterName, perms) - } - }, - Example: ` -kubectl pg add-user user01 -p login,createdb -c cluster01 -`, -} - -// add user to the cluster with provided permissions -func addUser(user string, clusterName string, permissions []string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - namespace := getCurrentNamespace() - postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - setUsers := make(map[string]bool) - for _, k := range permissions { - setUsers[k] = true - } - - if existingRoles, key := postgresql.Spec.Users[user]; key { - for _, k := range existingRoles { - setUsers[k] = true - } - } - - Privileges := []string{} - for keys, values := range setUsers { - if values { - Privileges = append(Privileges, keys) - } - } - - patch := applyUserPatch(user, Privileges) - updatedPostgresql, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patch, metav1.PatchOptions{}) - if err != nil { - log.Fatal(err) - } - - if updatedPostgresql.ResourceVersion != postgresql.ResourceVersion { - fmt.Printf("postgresql %s is updated with new user %s and with privileges %s.\n", updatedPostgresql.Name, user, permissions) - } else { - fmt.Printf("postgresql %s is unchanged.\n", updatedPostgresql.Name) - } -} - -func applyUserPatch(user string, value []string) []byte { - ins := map[string]map[string]map[string][]string{"spec": {"users": {user: value}}} - patchInstances, err := json.Marshal(ins) - if err != nil { - log.Fatal(err, "unable to parse number of instances json") - } - return patchInstances -} - -func init() { - addUserCmd.Flags().StringP("cluster", "c", "", "add user to the provided cluster.") - addUserCmd.Flags().StringP("privileges", "p", "", "add privileges separated by commas without spaces") - rootCmd.AddCommand(addUserCmd) -} diff --git a/kubectl-pg/cmd/check.go b/kubectl-pg/cmd/check.go deleted file mode 100644 index 6068c35bb..000000000 --- a/kubectl-pg/cmd/check.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "fmt" - "log" - - "github.com/spf13/cobra" - postgresConstants "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// checkCmd represent kubectl pg check. -var checkCmd = &cobra.Command{ - Use: "check", - Short: "Checks the Postgres operator is installed in the k8s cluster", - Long: `Checks that the Postgres CRD is registered in a k8s cluster. -This means that the operator pod was able to start normally.`, - Run: func(cmd *cobra.Command, args []string) { - check() - }, - Example: ` -kubectl pg check -`, -} - -// check validates postgresql CRD registered or not. -func check() *v1.CustomResourceDefinition { - config := getConfig() - apiExtClient, err := apiextv1.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - crdInfo, err := apiExtClient.CustomResourceDefinitions().Get(context.TODO(), postgresConstants.PostgresCRDResouceName, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - if crdInfo.Name == postgresConstants.PostgresCRDResouceName { - fmt.Printf("Postgres Operator is installed in the k8s cluster.\n") - } else { - fmt.Printf("Postgres Operator is not installed in the k8s cluster.\n") - } - return crdInfo -} - -func init() { - rootCmd.AddCommand(checkCmd) -} diff --git a/kubectl-pg/cmd/connect.go b/kubectl-pg/cmd/connect.go deleted file mode 100644 index a7643ca05..000000000 --- a/kubectl-pg/cmd/connect.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "log" - "os" - user "os/user" - - "github.com/spf13/cobra" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/remotecommand" -) - -// connectCmd represents the kubectl pg connect command -var connectCmd = &cobra.Command{ - Use: "connect", - Short: "Connects to the shell prompt, psql prompt of postgres cluster", - Long: `Connects to the shell prompt, psql prompt of postgres cluster and also to specified replica or master.`, - Run: func(cmd *cobra.Command, args []string) { - clusterName, _ := cmd.Flags().GetString("cluster") - master, _ := cmd.Flags().GetBool("master") - replica, _ := cmd.Flags().GetString("replica") - psql, _ := cmd.Flags().GetBool("psql") - userName, _ := cmd.Flags().GetString("user") - dbName, _ := cmd.Flags().GetString("database") - - if psql { - if userName == "" { - userInfo, err := user.Current() - if err != nil { - log.Fatal(err) - } - userName = userInfo.Username - } - } - if dbName == "" { - dbName = userName - } - - connect(clusterName, master, replica, psql, userName, dbName) - }, - Example: ` -#connects to the master of postgres cluster -kubectl pg connect -c cluster -m - -#connects to the random replica of postgres cluster -kubectl pg connect -c cluster - -#connects to the provided replica number of postgres cluster -kubectl pg connect -c cluster -r 2 - -#connects to psql prompt of master for provided postgres cluster with current shell user -kubectl pg connect -c cluster -p -m - -#connects to psql prompt of random replica for provided postgres cluster with provided user and db -kubectl pg connect -c cluster -p -u user01 -d db01 -`, -} - -func connect(clusterName string, master bool, replica string, psql bool, user string, dbName string) { - config := getConfig() - client, err := kubernetes.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - podName := getPodName(clusterName, master, replica) - var execRequest *rest.Request - - if psql { - execRequest = client.CoreV1().RESTClient().Post().Resource("pods"). - Name(podName). - Namespace(getCurrentNamespace()). - SubResource("exec"). - Param("container", "postgres"). - Param("command", "psql"). - Param("command", dbName). - Param("command", user). - Param("stdin", "true"). - Param("stdout", "true"). - Param("stderr", "true"). - Param("tty", "true") - } else { - execRequest = client.CoreV1().RESTClient().Post().Resource("pods"). - Name(podName). - Namespace(getCurrentNamespace()). - SubResource("exec"). - Param("container", "postgres"). - Param("command", "su"). - Param("command", "postgres"). - Param("stdin", "true"). - Param("stdout", "true"). - Param("stderr", "true"). - Param("tty", "true") - } - - exec, err := remotecommand.NewSPDYExecutor(config, "POST", execRequest.URL()) - if err != nil { - log.Fatal(err) - } - - err = exec.StreamWithContext(context.TODO(), remotecommand.StreamOptions{ - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - Tty: true, - }) - if err != nil { - log.Fatal(err) - } -} - -func init() { - connectCmd.Flags().StringP("cluster", "c", "", "provide the cluster name.") - connectCmd.Flags().BoolP("master", "m", false, "connect to master.") - connectCmd.Flags().StringP("replica", "r", "", "connect to replica. Specify replica number.") - connectCmd.Flags().BoolP("psql", "p", false, "connect to psql prompt.") - connectCmd.Flags().StringP("user", "u", "", "provide user.") - connectCmd.Flags().StringP("database", "d", "", "provide database name.") - rootCmd.AddCommand(connectCmd) -} diff --git a/kubectl-pg/cmd/create.go b/kubectl-pg/cmd/create.go deleted file mode 100644 index 3d34a7d25..000000000 --- a/kubectl-pg/cmd/create.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/spf13/cobra" - v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// createCmd kubectl pg create. -var createCmd = &cobra.Command{ - Use: "create", - Short: "Creates postgres object using manifest file", - Long: `Creates postgres custom resource objects from a manifest file.`, - Run: func(cmd *cobra.Command, args []string) { - fileName, _ := cmd.Flags().GetString("file") - create(fileName) - }, - Example: ` -kubectl pg create -f cluster-manifest.yaml -`, -} - -// Create postgresql resources. -func create(fileName string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - ymlFile, err := os.ReadFile(fileName) - if err != nil { - log.Fatal(err) - } - - decode := scheme.Codecs.UniversalDeserializer().Decode - obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{}) - if err != nil { - log.Fatal(err) - } - - postgresSql := obj.(*v1.Postgresql) - _, err = postgresConfig.Postgresqls(postgresSql.Namespace).Create(context.TODO(), postgresSql, metav1.CreateOptions{}) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("postgresql %s created.\n", postgresSql.Name) -} - -func init() { - createCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.") - rootCmd.AddCommand(createCmd) -} diff --git a/kubectl-pg/cmd/delete.go b/kubectl-pg/cmd/delete.go deleted file mode 100644 index 73a6e7b0b..000000000 --- a/kubectl-pg/cmd/delete.go +++ /dev/null @@ -1,134 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/spf13/cobra" - v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// deleteCmd represents kubectl pg delete. -var deleteCmd = &cobra.Command{ - Use: "delete", - Short: "Deletes postgresql object by cluster-name/manifest file", - Long: `Deletes the postgres objects identified by a manifest file or cluster-name. -Deleting the manifest is sufficient to delete the cluster.`, - Run: func(cmd *cobra.Command, args []string) { - namespace, _ := cmd.Flags().GetString("namespace") - file, _ := cmd.Flags().GetString("file") - - if file != "" { - deleteByFile(file) - } else if namespace != "" { - if len(args) != 0 { - clusterName := args[0] - deleteByName(clusterName, namespace) - } else { - fmt.Println("cluster name can't be empty") - } - } else { - fmt.Println("use the flag either -n or -f to delete a resource.") - } - }, - Example: ` -#Deleting the postgres cluster using manifest file -kubectl pg delete -f cluster-manifest.yaml - -#Deleting the postgres cluster using cluster name in current namespace. -kubectl pg delete cluster01 - -#Deleting the postgres cluster using cluster name in provided namespace -kubectl pg delete cluster01 -n namespace01 -`, -} - -func deleteByFile(file string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - ymlFile, err := os.ReadFile(file) - if err != nil { - log.Fatal(err) - } - - decode := scheme.Codecs.UniversalDeserializer().Decode - obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{}) - if err != nil { - log.Fatal(err) - } - - postgresSql := obj.(*v1.Postgresql) - _, err = postgresConfig.Postgresqls(postgresSql.Namespace).Get(context.TODO(), postgresSql.Name, metav1.GetOptions{}) - if err != nil { - fmt.Printf("Postgresql %s not found with the provided namespace %s : %s \n", postgresSql.Name, postgresSql.Namespace, err) - return - } - fmt.Printf("Are you sure you want to remove this PostgreSQL cluster? If so, please type (%s/%s) and hit Enter\n", postgresSql.Namespace, postgresSql.Name) - - confirmAction(postgresSql.Name, postgresSql.Namespace) - err = postgresConfig.Postgresqls(postgresSql.Namespace).Delete(context.TODO(), postgresSql.Name, metav1.DeleteOptions{}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Postgresql %s deleted from %s.\n", postgresSql.Name, postgresSql.Namespace) -} - -func deleteByName(clusterName string, namespace string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - _, err = postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{}) - if err != nil { - fmt.Printf("Postgresql %s not found with the provided namespace %s : %s \n", clusterName, namespace, err) - return - } - fmt.Printf("Are you sure you want to remove this PostgreSQL cluster? If so, please type (%s/%s) and hit Enter\n", namespace, clusterName) - - confirmAction(clusterName, namespace) - err = postgresConfig.Postgresqls(namespace).Delete(context.TODO(), clusterName, metav1.DeleteOptions{}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Postgresql %s deleted from %s.\n", clusterName, namespace) -} - -func init() { - namespace := getCurrentNamespace() - deleteCmd.Flags().StringP("namespace", "n", namespace, "namespace of the cluster to be deleted.") - deleteCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.") - rootCmd.AddCommand(deleteCmd) -} diff --git a/kubectl-pg/cmd/extVolume.go b/kubectl-pg/cmd/extVolume.go deleted file mode 100644 index 02ccc372d..000000000 --- a/kubectl-pg/cmd/extVolume.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "log" - "strconv" - - "github.com/spf13/cobra" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" -) - -// extVolumeCmd represents the extVolume command -var extVolumeCmd = &cobra.Command{ - Use: "ext-volume", - Short: "Increases the volume size of a given Postgres cluster", - Long: `Extends the volume of the postgres cluster. But volume cannot be shrinked.`, - Run: func(cmd *cobra.Command, args []string) { - clusterName, _ := cmd.Flags().GetString("cluster") - if len(args) > 0 { - volume := args[0] - extVolume(volume, clusterName) - } else { - fmt.Println("please enter the cluster name with -c flag & volume in desired units") - } - }, - Example: ` -#Extending the volume size of provided cluster -kubectl pg ext-volume 2Gi -c cluster01 -`, -} - -// extend volume with provided size & cluster name -func extVolume(increasedVolumeSize string, clusterName string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - namespace := getCurrentNamespace() - postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - oldSize, err := resource.ParseQuantity(postgresql.Spec.Volume.Size) - if err != nil { - log.Fatal(err) - } - - newSize, err := resource.ParseQuantity(increasedVolumeSize) - if err != nil { - log.Fatal(err) - } - - _, err = strconv.Atoi(newSize.String()) - if err == nil { - fmt.Println("provide the valid volume size with respective units i.e Ki, Mi, Gi") - return - } - - if newSize.Value() > oldSize.Value() { - patchInstances := volumePatch(newSize) - response, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patchInstances, metav1.PatchOptions{}) - if err != nil { - log.Fatal(err) - } - if postgresql.ResourceVersion != response.ResourceVersion { - fmt.Printf("%s volume is extended to %s.\n", response.Name, increasedVolumeSize) - } else { - fmt.Printf("%s volume %s is unchanged.\n", response.Name, postgresql.Spec.Volume.Size) - } - } else if newSize.Value() == oldSize.Value() { - fmt.Println("volume already has the desired size.") - } else { - fmt.Printf("volume %s size cannot be shrinked.\n", postgresql.Spec.Volume.Size) - } -} - -func volumePatch(volume resource.Quantity) []byte { - patchData := map[string]map[string]map[string]resource.Quantity{"spec": {"volume": {"size": volume}}} - patch, err := json.Marshal(patchData) - if err != nil { - log.Fatal(err, "unable to parse patch to extend volume") - } - return patch -} - -func init() { - extVolumeCmd.Flags().StringP("cluster", "c", "", "provide cluster name.") - rootCmd.AddCommand(extVolumeCmd) -} diff --git a/kubectl-pg/cmd/list.go b/kubectl-pg/cmd/list.go deleted file mode 100644 index 4fd6de3ba..000000000 --- a/kubectl-pg/cmd/list.go +++ /dev/null @@ -1,125 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "fmt" - "log" - "strconv" - "time" - - "github.com/spf13/cobra" - v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - TrimCreateTimestamp = 6000000000 -) - -// listCmd represents kubectl pg list. -var listCmd = &cobra.Command{ - Use: "list", - Short: "Lists all the resources of kind postgresql", - Long: `Lists all the info specific to postgresql objects.`, - Run: func(cmd *cobra.Command, args []string) { - allNamespaces, _ := cmd.Flags().GetBool("all-namespaces") - namespace, _ := cmd.Flags().GetString("namespace") - if allNamespaces { - list(allNamespaces, "") - } else { - list(allNamespaces, namespace) - } - - }, - Example: ` -#Lists postgres cluster in current namespace -kubectl pg list - -#Lists postgres clusters in all namespaces -kubectl pg list -A -`, -} - -// list command to list postgres. -func list(allNamespaces bool, namespace string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - listPostgres, err := postgresConfig.Postgresqls(namespace).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - log.Fatal(err) - } - - if len(listPostgres.Items) == 0 { - if namespace != "" { - fmt.Printf("No Postgresql clusters found in namespace: %v\n", namespace) - } else { - fmt.Println("No Postgresql clusters found in all namespaces") - } - return - } - - if allNamespaces { - listAll(listPostgres) - } else { - listWithNamespace(listPostgres) - } -} - -func listAll(listPostgres *v1.PostgresqlList) { - template := "%-32s%-16s%-12s%-12s%-12s%-12s%-12s\n" - fmt.Printf(template, "NAME", "STATUS", "INSTANCES", "VERSION", "AGE", "VOLUME", "NAMESPACE") - for _, pgObjs := range listPostgres.Items { - fmt.Printf(template, pgObjs.Name, - pgObjs.Status.PostgresClusterStatus, - strconv.Itoa(int(pgObjs.Spec.NumberOfInstances)), - pgObjs.Spec.PostgresqlParam.PgVersion, - time.Since(pgObjs.CreationTimestamp.Time).Truncate(TrimCreateTimestamp), - pgObjs.Spec.Size, pgObjs.Namespace) - } -} - -func listWithNamespace(listPostgres *v1.PostgresqlList) { - template := "%-32s%-16s%-12s%-12s%-12s%-12s\n" - fmt.Printf(template, "NAME", "STATUS", "INSTANCES", "VERSION", "AGE", "VOLUME") - for _, pgObjs := range listPostgres.Items { - fmt.Printf(template, pgObjs.Name, - pgObjs.Status.PostgresClusterStatus, - strconv.Itoa(int(pgObjs.Spec.NumberOfInstances)), - pgObjs.Spec.PostgresqlParam.PgVersion, - time.Since(pgObjs.CreationTimestamp.Time).Truncate(TrimCreateTimestamp), - pgObjs.Spec.Size) - } -} - -func init() { - listCmd.Flags().BoolP("all-namespaces", "A", false, "list pg resources across all namespaces.") - listCmd.Flags().StringP("namespace", "n", getCurrentNamespace(), "provide the namespace") - rootCmd.AddCommand(listCmd) -} diff --git a/kubectl-pg/cmd/logs.go b/kubectl-pg/cmd/logs.go deleted file mode 100644 index 21a4fd6ec..000000000 --- a/kubectl-pg/cmd/logs.go +++ /dev/null @@ -1,143 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "io" - "log" - "os" - - "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -// logsCmd represents the logs command -var logsCmd = &cobra.Command{ - Use: "logs", - Short: "This will fetch the logs of the specified postgres cluster & postgres operator", - Long: `Fetches the logs of the postgres cluster (i.e master( with -m flag) & replica with (-r 1 pod number) and without -m or -r connects to random replica`, - Run: func(cmd *cobra.Command, args []string) { - opLogs, _ := cmd.Flags().GetBool("operator") - clusterName, _ := cmd.Flags().GetString("cluster") - master, _ := cmd.Flags().GetBool("master") - replica, _ := cmd.Flags().GetString("replica") - - if opLogs { - operatorLogs() - } else { - clusterLogs(clusterName, master, replica) - } - }, - Example: ` -#Fetch the logs of the postgres operator -kubectl pg logs -o - -#Fetch the logs of the master for provided cluster -kubectl pg logs -c cluster01 -m - -#Fetch the logs of the random replica for provided cluster -kubectl pg logs -c cluster01 - -#Fetch the logs of the provided replica number of the cluster -kubectl pg logs -c cluster01 -r 3 -`, -} - -func operatorLogs() { - config := getConfig() - client, err := kubernetes.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - operator := getPostgresOperator(client) - allPods, err := client.CoreV1().Pods(operator.Namespace).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - log.Fatal(err) - } - - var operatorPodName string - for _, pod := range allPods.Items { - for key, value := range pod.Labels { - if (key == "name" && value == OperatorName) || (key == "app.kubernetes.io/name" && value == OperatorName) { - operatorPodName = pod.Name - break - } - } - } - - execRequest := client.CoreV1().RESTClient().Get().Namespace(operator.Namespace). - Name(operatorPodName). - Resource("pods"). - SubResource("log"). - Param("follow", "--follow"). - Param("container", OperatorName) - - readCloser, err := execRequest.Stream(context.TODO()) - if err != nil { - log.Fatal(err) - } - - defer readCloser.Close() - _, err = io.Copy(os.Stdout, readCloser) - if err != nil { - log.Fatal(err) - } -} - -func clusterLogs(clusterName string, master bool, replica string) { - config := getConfig() - client, err := kubernetes.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - podName := getPodName(clusterName, master, replica) - execRequest := client.CoreV1().RESTClient().Get().Namespace(getCurrentNamespace()). - Name(podName). - Resource("pods"). - SubResource("log"). - Param("follow", "--follow"). - Param("container", "postgres") - - readCloser, err := execRequest.Stream(context.TODO()) - if err != nil { - log.Fatal(err) - } - - defer readCloser.Close() - _, err = io.Copy(os.Stdout, readCloser) - if err != nil { - log.Fatal(err) - } -} - -func init() { - rootCmd.AddCommand(logsCmd) - logsCmd.Flags().BoolP("operator", "o", false, "logs of operator") - logsCmd.Flags().StringP("cluster", "c", "", "logs for the provided cluster") - logsCmd.Flags().BoolP("master", "m", false, "Patroni logs of master") - logsCmd.Flags().StringP("replica", "r", "", "Patroni logs of replica. Specify replica number.") -} diff --git a/kubectl-pg/cmd/root.go b/kubectl-pg/cmd/root.go deleted file mode 100644 index 163d6f6ea..000000000 --- a/kubectl-pg/cmd/root.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var rootCmd = &cobra.Command{ - Use: "kubectl-pg", - Short: "kubectl plugin for the Zalando Postgres operator.", - Long: `kubectl pg plugin for interaction with Zalando postgres operator.`, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { - viper.SetDefault("author", "Vineeth Pothulapati ") - viper.SetDefault("license", "mit") -} diff --git a/kubectl-pg/cmd/scale.go b/kubectl-pg/cmd/scale.go deleted file mode 100644 index 0a7bdc60f..000000000 --- a/kubectl-pg/cmd/scale.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "log" - "strconv" - - "github.com/spf13/cobra" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -// scaleCmd represents the scale command -var scaleCmd = &cobra.Command{ - Use: "scale", - Short: "Add/remove pods to a Postgres cluster", - Long: `Scales the postgres objects using cluster-name. -Scaling to 0 leads to down time.`, - Run: func(cmd *cobra.Command, args []string) { - clusterName, err := cmd.Flags().GetString("cluster") - if err != nil { - log.Fatal(err) - } - namespace, err := cmd.Flags().GetString("namespace") - if err != nil { - log.Fatal(err) - } - - if len(args) > 0 { - numberOfInstances, err := strconv.Atoi(args[0]) - if err != nil { - log.Fatal(err) - } - scale(int32(numberOfInstances), clusterName, namespace) - } else { - fmt.Println("Please enter number of instances to scale.") - } - - }, - Example: ` -#Usage -kubectl pg scale [NUMBER-OF-INSTANCES] -c [CLUSTER-NAME] -n [NAMESPACE] - -#Scales the number of instances of the provided cluster -kubectl pg scale 5 -c cluster01 -`, -} - -func scale(numberOfInstances int32, clusterName string, namespace string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - minInstances, maxInstances := allowedMinMaxInstances(config) - - if minInstances == -1 && maxInstances == -1 { - postgresql.Spec.NumberOfInstances = numberOfInstances - } else if numberOfInstances <= maxInstances && numberOfInstances >= minInstances { - postgresql.Spec.NumberOfInstances = numberOfInstances - } else if minInstances == -1 && numberOfInstances < postgresql.Spec.NumberOfInstances || - maxInstances == -1 && numberOfInstances > postgresql.Spec.NumberOfInstances { - postgresql.Spec.NumberOfInstances = numberOfInstances - } else { - log.Fatalf("cannot scale to the provided instances as they don't adhere to MIN_INSTANCES: %v and MAX_INSTANCES: %v provided in configmap or operatorconfiguration", maxInstances, minInstances) - } - - if numberOfInstances == 0 { - fmt.Printf("Scaling to zero leads to down time. please type %s/%s and hit Enter this serves to confirm the action\n", namespace, clusterName) - confirmAction(clusterName, namespace) - } - - patchInstances := scalePatch(numberOfInstances) - UpdatedPostgres, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patchInstances, metav1.PatchOptions{}) - if err != nil { - log.Fatal(err) - } - - if UpdatedPostgres.ResourceVersion != postgresql.ResourceVersion { - fmt.Printf("scaled postgresql %s/%s to %d instances\n", UpdatedPostgres.Namespace, UpdatedPostgres.Name, UpdatedPostgres.Spec.NumberOfInstances) - return - } - fmt.Printf("postgresql %s is unchanged.\n", postgresql.Name) -} - -func scalePatch(value int32) []byte { - instances := map[string]map[string]int32{"spec": {"numberOfInstances": value}} - patchInstances, err := json.Marshal(instances) - if err != nil { - log.Fatal(err, "unable to parse patch for scale") - } - return patchInstances -} - -func allowedMinMaxInstances(config *rest.Config) (int32, int32) { - k8sClient, err := kubernetes.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - operator := getPostgresOperator(k8sClient) - - operatorContainer := operator.Spec.Template.Spec.Containers - var configMapName, operatorConfigName string - // -1 indicates no limitations for min/max instances - minInstances := -1 - maxInstances := -1 - for _, envData := range operatorContainer[0].Env { - if envData.Name == "CONFIG_MAP_NAME" { - configMapName = envData.Value - } - if envData.Name == "POSTGRES_OPERATOR_CONFIGURATION_OBJECT" { - operatorConfigName = envData.Value - } - } - - if operatorConfigName == "" { - configMap, err := k8sClient.CoreV1().ConfigMaps(operator.Namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - configMapData := configMap.Data - for key, value := range configMapData { - if key == "min_instances" { - minInstances, err = strconv.Atoi(value) - if err != nil { - log.Fatalf("invalid min instances in configmap %v", err) - } - } - - if key == "max_instances" { - maxInstances, err = strconv.Atoi(value) - if err != nil { - log.Fatalf("invalid max instances in configmap %v", err) - } - } - } - } else if configMapName == "" { - pgClient, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - operatorConfig, err := pgClient.OperatorConfigurations(operator.Namespace).Get(context.TODO(), operatorConfigName, metav1.GetOptions{}) - if err != nil { - log.Fatalf("unable to read operator configuration %v", err) - } - - minInstances = int(operatorConfig.Configuration.MinInstances) - maxInstances = int(operatorConfig.Configuration.MaxInstances) - } - return int32(minInstances), int32(maxInstances) -} - -func init() { - namespace := getCurrentNamespace() - scaleCmd.Flags().StringP("namespace", "n", namespace, "namespace of the cluster to be scaled") - scaleCmd.Flags().StringP("cluster", "c", "", "provide the cluster name.") - rootCmd.AddCommand(scaleCmd) -} diff --git a/kubectl-pg/cmd/update.go b/kubectl-pg/cmd/update.go deleted file mode 100644 index eb9259586..000000000 --- a/kubectl-pg/cmd/update.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/spf13/cobra" - v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// updateCmd represents kubectl pg update -var updateCmd = &cobra.Command{ - Use: "update", - Short: "Updates postgresql object using manifest file", - Long: `Updates the state of cluster using manifest file to reflect the changes on the cluster.`, - Run: func(cmd *cobra.Command, args []string) { - fileName, _ := cmd.Flags().GetString("file") - updatePgResources(fileName) - }, - Example: ` -#usage -kubectl pg update -f [File-NAME] - -#update the postgres cluster with updated manifest file -kubectl pg update -f cluster-manifest.yaml -`, -} - -// Update postgresql resources. -func updatePgResources(fileName string) { - config := getConfig() - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - ymlFile, err := os.ReadFile(fileName) - if err != nil { - log.Fatal(err) - } - - decode := scheme.Codecs.UniversalDeserializer().Decode - obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{}) - if err != nil { - log.Fatal(err) - } - - newPostgresObj := obj.(*v1.Postgresql) - oldPostgresObj, err := postgresConfig.Postgresqls(newPostgresObj.Namespace).Get(context.TODO(), newPostgresObj.Name, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - newPostgresObj.ResourceVersion = oldPostgresObj.ResourceVersion - response, err := postgresConfig.Postgresqls(newPostgresObj.Namespace).Update(context.TODO(), newPostgresObj, metav1.UpdateOptions{}) - if err != nil { - log.Fatal(err) - } - - if newPostgresObj.ResourceVersion != response.ResourceVersion { - fmt.Printf("postgresql %s updated.\n", response.Name) - } else { - fmt.Printf("postgresql %s is unchanged.\n", response.Name) - } -} - -func init() { - updateCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.") - rootCmd.AddCommand(updateCmd) -} diff --git a/kubectl-pg/cmd/util.go b/kubectl-pg/cmd/util.go deleted file mode 100644 index fa0eb6d42..000000000 --- a/kubectl-pg/cmd/util.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "context" - "flag" - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - - PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" - v1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" -) - -const ( - OperatorName = "postgres-operator" - DefaultNamespace = "default" -) - -func getConfig() *restclient.Config { - var kubeconfig *string - var config *restclient.Config - envKube := os.Getenv("KUBECONFIG") - if envKube != "" { - kubeconfig = &envKube - } else { - if home := homedir.HomeDir(); home != "" { - kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") - } else { - kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") - } - } - flag.Parse() - var err error - config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig) - if err != nil { - log.Fatal(err) - } - return config -} - -func getCurrentNamespace() string { - namespace, err := exec.Command("kubectl", "config", "view", "--minify", "--output", "jsonpath={..namespace}").CombinedOutput() - if err != nil { - log.Fatal(err) - } - currentNamespace := string(namespace) - if currentNamespace == "" { - currentNamespace = DefaultNamespace - } - return currentNamespace -} - -func confirmAction(clusterName string, namespace string) { - for { - confirmClusterDetails := "" - _, err := fmt.Scan(&confirmClusterDetails) - if err != nil { - log.Fatalf("couldn't get confirmation from the user %v", err) - } - clusterDetails := strings.Split(confirmClusterDetails, "/") - if clusterDetails[0] != namespace || clusterDetails[1] != clusterName { - fmt.Printf("cluster name or namespace does not match. Please re-enter %s/%s\nHint: Press (ctrl+c) to exit\n", namespace, clusterName) - } else { - return - } - } -} - -func getPodName(clusterName string, master bool, replicaNumber string) string { - config := getConfig() - client, err := kubernetes.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - postgresConfig, err := PostgresqlLister.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - postgresCluster, err := postgresConfig.Postgresqls(getCurrentNamespace()).Get(context.TODO(), clusterName, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - numOfInstances := postgresCluster.Spec.NumberOfInstances - var podName string - var podRole string - replica := clusterName + "-" + replicaNumber - - for ins := 0; ins < int(numOfInstances); ins++ { - pod, err := client.CoreV1().Pods(getCurrentNamespace()).Get(context.TODO(), clusterName+"-"+strconv.Itoa(ins), metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - - podRole = pod.Labels["spilo-role"] - if podRole == "master" && master { - podName = pod.Name - fmt.Printf("connected to %s with pod name as %s\n", podRole, podName) - break - } else if podRole == "replica" && !master && (pod.Name == replica || replicaNumber == "") { - podName = pod.Name - fmt.Printf("connected to %s with pod name as %s\n", podRole, podName) - break - } - } - if podName == "" { - log.Fatal("Provided replica doesn't exist") - } - return podName -} - -func getPostgresOperator(k8sClient *kubernetes.Clientset) *v1.Deployment { - var operator *v1.Deployment - operator, err := k8sClient.AppsV1().Deployments(getCurrentNamespace()).Get(context.TODO(), OperatorName, metav1.GetOptions{}) - if err == nil { - return operator - } - - allDeployments := k8sClient.AppsV1().Deployments("") - listDeployments, err := allDeployments.List(context.TODO(), metav1.ListOptions{}) - if err != nil { - log.Fatal(err) - } - - for _, deployment := range listDeployments.Items { - if deployment.Name == OperatorName { - operator = deployment.DeepCopy() - break - } else { - for key, value := range deployment.Labels { - if key == "app.kubernetes.io/name" && value == OperatorName { - operator = deployment.DeepCopy() - break - } - } - } - } - return operator -} diff --git a/kubectl-pg/cmd/version.go b/kubectl-pg/cmd/version.go deleted file mode 100644 index 23cc55422..000000000 --- a/kubectl-pg/cmd/version.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package cmd - -import ( - "fmt" - "log" - "strings" - - "github.com/spf13/cobra" - "k8s.io/client-go/kubernetes" -) - -var KubectlPgVersion string = "1.0" - -// versionCmd represents the version command -var versionCmd = &cobra.Command{ - Use: "version", - Short: "version of kubectl-pg & postgres-operator", - Long: `version of kubectl-pg and current running postgres-operator`, - Run: func(cmd *cobra.Command, args []string) { - namespace, err := cmd.Flags().GetString("namespace") - if err != nil { - log.Fatal(err) - } - version(namespace) - }, - Example: ` -#Lists the version of kubectl pg plugin and postgres operator in current namespace -kubectl pg version - -#Lists the version of kubectl pg plugin and postgres operator in provided namespace -kubectl pg version -n namespace01 -`, -} - -func version(namespace string) { - fmt.Printf("kubectl-pg: %s\n", KubectlPgVersion) - - config := getConfig() - client, err := kubernetes.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - operatorDeployment := getPostgresOperator(client) - if operatorDeployment.Name == "" { - log.Fatalf("make sure zalando's postgres operator is running in namespace %s", namespace) - } - operatorImage := operatorDeployment.Spec.Template.Spec.Containers[0].Image - imageDetails := strings.Split(operatorImage, ":") - imageSplit := len(imageDetails) - imageVersion := imageDetails[imageSplit-1] - fmt.Printf("Postgres-Operator: %s\n", imageVersion) -} - -func init() { - rootCmd.AddCommand(versionCmd) - versionCmd.Flags().StringP("namespace", "n", DefaultNamespace, "provide the namespace.") -} diff --git a/kubectl-pg/go.mod b/kubectl-pg/go.mod deleted file mode 100644 index 7f80cbfd7..000000000 --- a/kubectl-pg/go.mod +++ /dev/null @@ -1,72 +0,0 @@ -module github.com/zalando/postgres-operator/kubectl-pg - -go 1.25.3 - -require ( - github.com/spf13/cobra v1.10.1 - github.com/spf13/viper v1.21.0 - github.com/zalando/postgres-operator v1.15.0 - k8s.io/api v0.32.9 - k8s.io/apiextensions-apiserver v0.25.9 - k8s.io/apimachinery v0.32.9 - k8s.io/client-go v0.32.9 -) - -require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/moby/spdystream v0.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - 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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/x448/float16 v0.8.4 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.9.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect -) diff --git a/kubectl-pg/go.sum b/kubectl-pg/go.sum deleted file mode 100644 index 488d24edc..000000000 --- a/kubectl-pg/go.sum +++ /dev/null @@ -1,206 +0,0 @@ -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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -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= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d h1:LznySqW8MqVeFh+pW6rOkFdld9QQ7jRydBKKM6jyPVI= -github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d/go.mod h1:u3hJ0kqCQu/cPpsu3RbCOPZ0d7V3IjPjv1adNRleM9I= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -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/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= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -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.15.0 h1:is/7cOrpuV7OwMiN7TG7GgiYHKvaWx8Ptw3hJruFO1I= -github.com/zalando/postgres-operator v1.15.0/go.mod h1:1cSOA5dG2dEqdG0uami1RHTGYX92bgAKYASfAhuMtHE= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -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/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -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/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -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.32.9 h1:q/59kk8lnecgG0grJqzrmXC1Jcl2hPWp9ltz0FQuoLI= -k8s.io/api v0.32.9/go.mod h1:jIfT3rwW4EU1IXZm9qjzSk/2j91k4CJL5vUULrxqp3Y= -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.32.9 h1:fXk8ktfsxrdThaEOAQFgkhCK7iyoyvS8nbYJ83o/SSs= -k8s.io/apimachinery v0.32.9/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.9 h1:ZMyIQ1TEpTDAQni3L2gH1NZzyOA/gHfNcAazzCxMJ0c= -k8s.io/client-go v0.32.9/go.mod h1:2OT8aFSYvUjKGadaeT+AVbhkXQSpMAkiSb88Kz2WggI= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/kubectl-pg/main.go b/kubectl-pg/main.go deleted file mode 100644 index bfcd5eb29..000000000 --- a/kubectl-pg/main.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -package main - -import ( - "github.com/zalando/postgres-operator/kubectl-pg/cmd" -) - -func main() { - cmd.Execute() -} From 1036350eb4d51106c07030cd033354fb073d0d55 Mon Sep 17 00:00:00 2001 From: Raphael Torquato <89878688+raphaeltorquat0@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:09:06 -0300 Subject: [PATCH 29/70] feat: add IPv6 support to allowedSourceRanges (#3082) * feat: add IPv6 support to allowedSourceRanges Update regex pattern in CRD validation to accept both IPv4 and IPv6 CIDR notation, enabling dual-stack networking support. Fixes #2787 Signed-off-by: Raphael Torquato <> * add unit test fror ipv6 allowedSourceRanges --------- Signed-off-by: Raphael Torquato <> Co-authored-by: Raphael Torquato <> Co-authored-by: Jociele Padilha --- .../postgres-operator/crds/postgresqls.yaml | 2 +- docs/reference/cluster_manifest.md | 9 ++-- manifests/postgresql.crd.yaml | 2 +- pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 2 +- pkg/apis/acid.zalan.do/v1/util_test.go | 45 +++++++++++++++++++ 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index a52259a85..4d7c2586a 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -107,7 +107,7 @@ spec: description: load balancers' source ranges are the same for master and replica services items: - pattern: ^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$ + pattern: '^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$' type: string nullable: true type: array diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index b216c1fb2..0717e411f 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -113,10 +113,11 @@ These parameters are grouped directly under the `spec` key in the manifest. * **allowedSourceRanges** when one or more load balancers are enabled for the cluster, this parameter - defines the comma-separated range of IP networks (in CIDR-notation). The - corresponding load balancer is accessible only to the networks defined by - this parameter. Optional, when empty the load balancer service becomes - inaccessible from outside of the Kubernetes cluster. + defines the comma-separated range of IP networks (in CIDR-notation). Both + IPv4 (e.g. `192.168.1.0/24`) and IPv6 (e.g. `fd01::/48`) CIDR ranges are + supported. The corresponding load balancer is accessible only to the networks + defined by this parameter. Optional, when empty the load balancer service + becomes inaccessible from outside of the Kubernetes cluster. * **enableMasterNodePort** boolean flag to override the operator defaults (set by the diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index b9652ef22..b139c1db9 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -108,7 +108,7 @@ spec: description: load balancers' source ranges are the same for master and replica services items: - pattern: ^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$ + pattern: ^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$ type: string nullable: true type: array diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index b9652ef22..b139c1db9 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -108,7 +108,7 @@ spec: description: load balancers' source ranges are the same for master and replica services items: - pattern: ^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$ + pattern: ^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$ type: string nullable: true type: array diff --git a/pkg/apis/acid.zalan.do/v1/util_test.go b/pkg/apis/acid.zalan.do/v1/util_test.go index fcc5ae5fd..857622581 100644 --- a/pkg/apis/acid.zalan.do/v1/util_test.go +++ b/pkg/apis/acid.zalan.do/v1/util_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "reflect" + "regexp" "testing" "time" @@ -810,3 +811,47 @@ func TestPostgresqlClone(t *testing.T) { }) } } + +func TestAllowedSourceRangesPattern(t *testing.T) { + // pattern used in CRD validation for allowedSourceRanges + pattern := `^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$` + re := regexp.MustCompile(pattern) + + valid := []string{ + // IPv4 + "192.168.1.0/24", + "0.0.0.0/0", + "127.0.0.1/32", + "10.0.0.0/8", + "185.85.220.0/22", + // IPv6 + "fd01::/48", + "::1/128", + "::/0", + "2001:db8::/32", + "fe80::1/64", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128", + } + + invalid := []string{ + "999.999.999.999/24", + "192.168.1.0/33", + "192.168.1.0", + "not-an-ip", + "fd01::/129", + "::gggg/64", + "", + } + + for _, cidr := range valid { + if !re.MatchString(cidr) { + t.Errorf("expected %q to match allowedSourceRanges pattern", cidr) + } + } + + for _, cidr := range invalid { + if re.MatchString(cidr) { + t.Errorf("expected %q NOT to match allowedSourceRanges pattern", cidr) + } + } +} From a30e15e47204830a9caf7bde3bbc1d10a6c9b27d Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Fri, 12 Jun 2026 15:44:55 +0200 Subject: [PATCH 30/70] re-generate CRDs to latest state (#3109) --- .../postgres-operator/crds/postgresqls.yaml | 40 ++++++++++--------- manifests/postgresql.crd.yaml | 24 +++++++++++ pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 24 +++++++++++ 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 4d7c2586a..568ad28ac 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -273,34 +273,26 @@ spec: vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest in that case the var evaluates to nil and the value is taken from the operator config type: boolean + enableMasterNodePort: + description: |- + vars to enable and configure nodeport services + set ports to 0 or nil to let kubernetes decide which port to use + overrides loadbalancer configuration + type: boolean enableMasterPoolerLoadBalancer: type: boolean + enableMasterPoolerNodePort: + type: boolean enableReplicaConnectionPooler: type: boolean enableReplicaLoadBalancer: type: boolean - enableReplicaPoolerLoadBalancer: - type: boolean - enableMasterNodePort: - type: boolean - masterNodePort: - type: integer - minimum: 0 - enableMasterPoolerNodePort: - type: boolean - masterPoolerNodePort: - type: integer - minimum: 0 enableReplicaNodePort: type: boolean - replicaNodePort: - type: integer - minimum: 0 + enableReplicaPoolerLoadBalancer: + type: boolean enableReplicaPoolerNodePort: type: boolean - replicaPoolerNodePort: - type: integer - minimum: 0 enableShmVolume: type: boolean env: @@ -3428,6 +3420,12 @@ spec: 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))\ *$' type: string type: array + masterNodePort: + format: int32 + type: integer + masterPoolerNodePort: + format: int32 + type: integer masterServiceAnnotations: additionalProperties: type: string @@ -3732,6 +3730,12 @@ spec: replicaLoadBalancer: description: deprecated type: boolean + replicaNodePort: + format: int32 + type: integer + replicaPoolerNodePort: + format: int32 + type: integer replicaServiceAnnotations: additionalProperties: type: string diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index b139c1db9..72d7153ba 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -274,14 +274,26 @@ spec: vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest in that case the var evaluates to nil and the value is taken from the operator config type: boolean + enableMasterNodePort: + description: |- + vars to enable and configure nodeport services + set ports to 0 or nil to let kubernetes decide which port to use + overrides loadbalancer configuration + type: boolean enableMasterPoolerLoadBalancer: type: boolean + enableMasterPoolerNodePort: + type: boolean enableReplicaConnectionPooler: type: boolean enableReplicaLoadBalancer: type: boolean + enableReplicaNodePort: + type: boolean enableReplicaPoolerLoadBalancer: type: boolean + enableReplicaPoolerNodePort: + type: boolean enableShmVolume: type: boolean env: @@ -3409,6 +3421,12 @@ spec: 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))\ *$' type: string type: array + masterNodePort: + format: int32 + type: integer + masterPoolerNodePort: + format: int32 + type: integer masterServiceAnnotations: additionalProperties: type: string @@ -3713,6 +3731,12 @@ spec: replicaLoadBalancer: description: deprecated type: boolean + replicaNodePort: + format: int32 + type: integer + replicaPoolerNodePort: + format: int32 + type: integer replicaServiceAnnotations: additionalProperties: type: string diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index b139c1db9..72d7153ba 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -274,14 +274,26 @@ spec: vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest in that case the var evaluates to nil and the value is taken from the operator config type: boolean + enableMasterNodePort: + description: |- + vars to enable and configure nodeport services + set ports to 0 or nil to let kubernetes decide which port to use + overrides loadbalancer configuration + type: boolean enableMasterPoolerLoadBalancer: type: boolean + enableMasterPoolerNodePort: + type: boolean enableReplicaConnectionPooler: type: boolean enableReplicaLoadBalancer: type: boolean + enableReplicaNodePort: + type: boolean enableReplicaPoolerLoadBalancer: type: boolean + enableReplicaPoolerNodePort: + type: boolean enableShmVolume: type: boolean env: @@ -3409,6 +3421,12 @@ spec: 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))\ *$' type: string type: array + masterNodePort: + format: int32 + type: integer + masterPoolerNodePort: + format: int32 + type: integer masterServiceAnnotations: additionalProperties: type: string @@ -3713,6 +3731,12 @@ spec: replicaLoadBalancer: description: deprecated type: boolean + replicaNodePort: + format: int32 + type: integer + replicaPoolerNodePort: + format: int32 + type: integer replicaServiceAnnotations: additionalProperties: type: string From 49cde600b80a46b1e0c414e775a6a748937a7065 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Sat, 13 Jun 2026 00:14:53 +0200 Subject: [PATCH 31/70] update new source ranges validation for kubebuilder (#3110) --- charts/postgres-operator/crds/postgresqls.yaml | 2 +- pkg/apis/acid.zalan.do/v1/postgresql_type.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 568ad28ac..2bbf5ee49 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -107,7 +107,7 @@ spec: description: load balancers' source ranges are the same for master and replica services items: - pattern: '^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$' + pattern: ^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$ type: string nullable: true type: array diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 71ac73133..81efa42cc 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -83,7 +83,7 @@ type PostgresSpec struct { // load balancers' source ranges are the same for master and replica services // +nullable - // +kubebuilder:validation:items:Pattern=`^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$` + // +kubebuilder:validation:items:Pattern=`^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$` // +optional AllowedSourceRanges []string `json:"allowedSourceRanges"` From e4e686588e7016f17ce9dfa544794f1bd09bd17f Mon Sep 17 00:00:00 2001 From: Jociele Padilha <45459238+jopadi@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:12:58 +0200 Subject: [PATCH 32/70] Fix/logical backup job cleanup (#3111) * feat(logical-backup): add configurable job history limits and TTL Adds three new configuration options for logical backup cronjobs: - logical_backup_successful_jobs_history_limit (default: 3) - logical_backup_failed_jobs_history_limit (default: 3) - logical_backup_ttl_seconds_after_finished (default: 86400) These options control how many completed/failed backup jobs are retained by Kubernetes and when finished jobs are automatically deleted. This prevents accumulation of old backup jobs and pods in namespaces with many PostgreSQL clusters. Also updates the CronJob comparison logic to detect changes in these new fields and trigger reconciliation when needed. Closes zalando/postgres-operator#1092 * add added the 3 new fieldson crd * updated gen api --------- Co-authored-by: Jairo Llopis Co-authored-by: Felix Kunde --- .../crds/operatorconfigurations.yaml | 12 ++++++++ charts/postgres-operator/values.yaml | 6 ++++ docs/reference/operator_parameters.md | 9 ++++++ manifests/operatorconfiguration.crd.yaml | 12 ++++++++ pkg/apis/acid.zalan.do/v1/crds.go | 9 ++++++ .../v1/operator_configuration_type.go | 3 ++ .../acid.zalan.do/v1/zz_generated.deepcopy.go | 17 ++++++++++- pkg/cluster/cluster.go | 15 ++++++++++ pkg/cluster/cluster_test.go | 11 +++++++- pkg/cluster/k8sres.go | 28 ++++++++++++++++--- pkg/cluster/k8sres_test.go | 26 +++++++++++++++++ pkg/controller/operator_config.go | 3 ++ pkg/util/config/config.go | 3 ++ 13 files changed, 148 insertions(+), 6 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 09356c476..1414366ff 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -726,6 +726,18 @@ spec: default: "30 00 * * *" logical_backup_cronjob_environment_secret: type: string + logical_backup_failed_jobs_history_limit: + type: integer + minimum: 0 + default: 3 + logical_backup_successful_jobs_history_limit: + type: integer + minimum: 0 + default: 3 + logical_backup_ttl_seconds_after_finished: + type: integer + minimum: 0 + default: 86400 debug: type: object properties: diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 82e9ac342..bb2831dd3 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -415,6 +415,12 @@ configLogicalBackup: logical_backup_schedule: "30 00 * * *" # secret to be used as reference for env variables in cronjob logical_backup_cronjob_environment_secret: "" + # number of successful backup jobs to keep in cronjob history + logical_backup_successful_jobs_history_limit: 3 + # number of failed backup jobs to keep in cronjob history + logical_backup_failed_jobs_history_limit: 3 + # TTL in seconds after which finished backup jobs are automatically deleted + logical_backup_ttl_seconds_after_finished: 86400 # automate creation of human users with teams API service configTeamsApi: diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 332742a16..d3d1fa742 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -904,6 +904,15 @@ grouped under the `logical_backup` key. * **logical_backup_cronjob_environment_secret** Reference to a Kubernetes secret, which keys will be added as environment variables to the cronjob. Default: "" +* **logical_backup_successful_jobs_history_limit** + number of successful backup jobs to keep in cronjob history. The default is `3`. + +* **logical_backup_failed_jobs_history_limit** + number of failed backup jobs to keep in cronjob history. The default is `3`. + +* **logical_backup_ttl_seconds_after_finished** + TTL in seconds after which finished backup jobs are automatically deleted. The default is `86400`. + The following environment variables can be passed to the logical backup cronjob via `logical_backup_cronjob_environment_secret` to control connectivity checks before the backup starts: diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 81bcd4381..fb009c459 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -712,6 +712,18 @@ spec: default: "30 00 * * *" logical_backup_cronjob_environment_secret: type: string + logical_backup_failed_jobs_history_limit: + type: integer + minimum: 0 + default: 3 + logical_backup_successful_jobs_history_limit: + type: integer + minimum: 0 + default: 3 + logical_backup_ttl_seconds_after_finished: + type: integer + minimum: 0 + default: 86400 debug: type: object properties: diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 6b43d0c54..867f573b3 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -899,6 +899,15 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "logical_backup_cronjob_environment_secret": { Type: "string", }, + "logical_backup_successful_jobs_history_limit": { + Type: "integer", + }, + "logical_backup_failed_jobs_history_limit": { + Type: "integer", + }, + "logical_backup_ttl_seconds_after_finished": { + Type: "integer", + }, }, }, "debug": { 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 3f28effc8..5cec0ff33 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -252,6 +252,9 @@ type OperatorLogicalBackupConfiguration struct { MemoryRequest string `json:"logical_backup_memory_request,omitempty"` CPULimit string `json:"logical_backup_cpu_limit,omitempty"` MemoryLimit string `json:"logical_backup_memory_limit,omitempty"` + SuccessfulJobsHistoryLimit *int32 `json:"logical_backup_successful_jobs_history_limit,omitempty"` + FailedJobsHistoryLimit *int32 `json:"logical_backup_failed_jobs_history_limit,omitempty"` + TTLSecondsAfterFinished *int32 `json:"logical_backup_ttl_seconds_after_finished,omitempty"` } // PatroniConfiguration defines configuration for Patroni 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 ff83abec9..9005b0bbe 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -480,7 +480,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData in.TeamsAPI.DeepCopyInto(&out.TeamsAPI) out.LoggingRESTAPI = in.LoggingRESTAPI out.Scalyr = in.Scalyr - out.LogicalBackup = in.LogicalBackup + in.LogicalBackup.DeepCopyInto(&out.LogicalBackup) in.ConnectionPooler.DeepCopyInto(&out.ConnectionPooler) in.Patroni.DeepCopyInto(&out.Patroni) return @@ -558,6 +558,21 @@ func (in *OperatorDebugConfiguration) DeepCopy() *OperatorDebugConfiguration { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperatorLogicalBackupConfiguration) DeepCopyInto(out *OperatorLogicalBackupConfiguration) { *out = *in + if in.SuccessfulJobsHistoryLimit != nil { + in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit + *out = new(int32) + **out = **in + } + if in.FailedJobsHistoryLimit != nil { + in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit + *out = new(int32) + **out = **in + } + if in.TTLSecondsAfterFinished != nil { + in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished + *out = new(int32) + **out = **in + } return } diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 6d9e6150a..a1e9f3c4f 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -919,6 +919,21 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) *compareLog reasons = append(reasons, fmt.Sprintf("logical backup container specs do not match: %v", strings.Join(contReasons, `', '`))) } + if !reflect.DeepEqual(cur.Spec.SuccessfulJobsHistoryLimit, new.Spec.SuccessfulJobsHistoryLimit) { + match = false + reasons = append(reasons, fmt.Sprintf("new job's successfulJobsHistoryLimit %v does not match the current one %v", new.Spec.SuccessfulJobsHistoryLimit, cur.Spec.SuccessfulJobsHistoryLimit)) + } + + if !reflect.DeepEqual(cur.Spec.FailedJobsHistoryLimit, new.Spec.FailedJobsHistoryLimit) { + match = false + reasons = append(reasons, fmt.Sprintf("new job's failedJobsHistoryLimit %v does not match the current one %v", new.Spec.FailedJobsHistoryLimit, cur.Spec.FailedJobsHistoryLimit)) + } + + if !reflect.DeepEqual(cur.Spec.JobTemplate.Spec.TTLSecondsAfterFinished, new.Spec.JobTemplate.Spec.TTLSecondsAfterFinished) { + match = false + reasons = append(reasons, fmt.Sprintf("new job's TTLSecondsAfterFinished %v does not match the current one %v", new.Spec.JobTemplate.Spec.TTLSecondsAfterFinished, cur.Spec.JobTemplate.Spec.TTLSecondsAfterFinished)) + } + return &compareLogicalBackupJobResult{match: match, reasons: reasons, deletedPodAnnotations: deletedPodAnnotations} } diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 5fdf1a220..95a445ff3 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -1567,12 +1567,21 @@ func TestCompareServices(t *testing.T) { } } +var ( + defaultSuccessfulJobsHistoryLimit = int32(3) + defaultFailedJobsHistoryLimit = int32(3) + defaultTTLSecondsAfterFinished = int32(86400) +) + func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMount) *batchv1.CronJob { cron := &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ - Schedule: schedule, + Schedule: schedule, + SuccessfulJobsHistoryLimit: &defaultSuccessfulJobsHistoryLimit, + FailedJobsHistoryLimit: &defaultFailedJobsHistoryLimit, JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: &defaultTTLSecondsAfterFinished, Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 302797dc4..5b668c108 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -2452,7 +2452,13 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { // configure a batch job jobSpec := batchv1.JobSpec{ - Template: *podTemplate, + Template: *podTemplate, + TTLSecondsAfterFinished: c.OpConfig.LogicalBackup.LogicalBackupTTLSecondsAfterFinished, + } + + if jobSpec.TTLSecondsAfterFinished == nil { + defaultTTL := int32(86400) + jobSpec.TTLSecondsAfterFinished = &defaultTTL } // configure a cron job @@ -2470,6 +2476,18 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { schedule = c.OpConfig.LogicalBackupSchedule } + successfulJobsHistoryLimit := c.OpConfig.LogicalBackup.LogicalBackupSuccessfulJobsHistoryLimit + if successfulJobsHistoryLimit == nil { + defaultLimit := int32(3) + successfulJobsHistoryLimit = &defaultLimit + } + + failedJobsHistoryLimit := c.OpConfig.LogicalBackup.LogicalBackupFailedJobsHistoryLimit + if failedJobsHistoryLimit == nil { + defaultLimit := int32(3) + failedJobsHistoryLimit = &defaultLimit + } + cronJob := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: c.getLogicalBackupJobName(), @@ -2479,9 +2497,11 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { OwnerReferences: c.ownerReferences(), }, Spec: batchv1.CronJobSpec{ - Schedule: schedule, - JobTemplate: jobTemplateSpec, - ConcurrencyPolicy: batchv1.ForbidConcurrent, + Schedule: schedule, + JobTemplate: jobTemplateSpec, + ConcurrencyPolicy: batchv1.ForbidConcurrent, + SuccessfulJobsHistoryLimit: successfulJobsHistoryLimit, + FailedJobsHistoryLimit: failedJobsHistoryLimit, }, } diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index bf21b8645..8cbecb2fa 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -4229,6 +4229,32 @@ func TestGenerateLogicalBackupJob(t *testing.T) { if !reflect.DeepEqual(tt.expectedResources, clusterResources) { t.Errorf("%s - %s: expected resources %#v, got %#v", t.Name(), tt.subTest, tt.expectedResources, clusterResources) } + + expectedSuccessfulJobsHistoryLimit := int32(3) + if cluster.OpConfig.LogicalBackup.LogicalBackupSuccessfulJobsHistoryLimit != nil { + expectedSuccessfulJobsHistoryLimit = *cluster.OpConfig.LogicalBackup.LogicalBackupSuccessfulJobsHistoryLimit + } + if *cronJob.Spec.SuccessfulJobsHistoryLimit != expectedSuccessfulJobsHistoryLimit { + t.Errorf("%s - %s: expected successfulJobsHistoryLimit %d, got %d", t.Name(), tt.subTest, expectedSuccessfulJobsHistoryLimit, *cronJob.Spec.SuccessfulJobsHistoryLimit) + } + + expectedFailedJobsHistoryLimit := int32(3) + if cluster.OpConfig.LogicalBackup.LogicalBackupFailedJobsHistoryLimit != nil { + expectedFailedJobsHistoryLimit = *cluster.OpConfig.LogicalBackup.LogicalBackupFailedJobsHistoryLimit + } + if *cronJob.Spec.FailedJobsHistoryLimit != expectedFailedJobsHistoryLimit { + t.Errorf("%s - %s: expected failedJobsHistoryLimit %d, got %d", t.Name(), tt.subTest, expectedFailedJobsHistoryLimit, *cronJob.Spec.FailedJobsHistoryLimit) + } + + expectedTTL := int32(86400) + if cluster.OpConfig.LogicalBackup.LogicalBackupTTLSecondsAfterFinished != nil { + expectedTTL = *cluster.OpConfig.LogicalBackup.LogicalBackupTTLSecondsAfterFinished + } + if cronJob.Spec.JobTemplate.Spec.TTLSecondsAfterFinished == nil { + t.Errorf("%s - %s: expected TTLSecondsAfterFinished to be set", t.Name(), tt.subTest) + } else if *cronJob.Spec.JobTemplate.Spec.TTLSecondsAfterFinished != expectedTTL { + t.Errorf("%s - %s: expected TTLSecondsAfterFinished %d, got %d", t.Name(), tt.subTest, expectedTTL, *cronJob.Spec.JobTemplate.Spec.TTLSecondsAfterFinished) + } } } diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 9d752a76e..4514e7487 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -217,6 +217,9 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.LogicalBackupMemoryRequest = fromCRD.LogicalBackup.MemoryRequest result.LogicalBackupCPULimit = fromCRD.LogicalBackup.CPULimit result.LogicalBackupMemoryLimit = fromCRD.LogicalBackup.MemoryLimit + result.LogicalBackupSuccessfulJobsHistoryLimit = util.CoalesceInt32(fromCRD.LogicalBackup.SuccessfulJobsHistoryLimit, k8sutil.Int32ToPointer(3)) + result.LogicalBackupFailedJobsHistoryLimit = util.CoalesceInt32(fromCRD.LogicalBackup.FailedJobsHistoryLimit, k8sutil.Int32ToPointer(3)) + result.LogicalBackupTTLSecondsAfterFinished = fromCRD.LogicalBackup.TTLSecondsAfterFinished // debug config result.DebugLogging = *util.CoalesceBool(fromCRD.OperatorDebug.DebugLogging, util.True()) diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 9a18e0d25..06edac439 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -149,6 +149,9 @@ type LogicalBackup struct { LogicalBackupMemoryRequest string `name:"logical_backup_memory_request"` LogicalBackupCPULimit string `name:"logical_backup_cpu_limit"` LogicalBackupMemoryLimit string `name:"logical_backup_memory_limit"` + LogicalBackupSuccessfulJobsHistoryLimit *int32 `name:"logical_backup_successful_jobs_history_limit" default:"3"` + LogicalBackupFailedJobsHistoryLimit *int32 `name:"logical_backup_failed_jobs_history_limit" default:"3"` + LogicalBackupTTLSecondsAfterFinished *int32 `name:"logical_backup_ttl_seconds_after_finished" default:"86400"` } // Operator options for connection pooler From a664816c0910975e10e573094172011ccb7fc3ab Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Mon, 22 Jun 2026 10:44:46 +0200 Subject: [PATCH 33/70] auto-generate configuration CRD (#3102) * auto-generate configuration CRD * make all subconfig optional * remove field enable crd validation * update field descriptions which use proxy types --- LICENSE | 2 +- Makefile | 4 +- .../crds/operatorconfigurations.yaml | 969 +++++++------- docs/reference/operator_parameters.md | 5 - manifests/configmap.yaml | 1 - manifests/operatorconfiguration.crd.yaml | 964 +++++++------- pkg/apis/acid.zalan.do/v1/crds.go | 1143 +---------------- .../v1/operator_configuration_type.go | 502 +++++--- .../v1/operatorconfiguration.crd.yaml | 971 ++++++++++++++ .../acid.zalan.do/v1/zz_generated.deepcopy.go | 5 - pkg/controller/operator_config.go | 1 - pkg/controller/util.go | 6 +- .../v1/fake/fake_operatorconfiguration.go | 12 +- .../acid.zalan.do/v1/operatorconfiguration.go | 14 +- .../acid.zalan.do/v1/interface.go | 7 + .../acid.zalan.do/v1/operatorconfiguration.go | 96 ++ .../informers/externalversions/generic.go | 2 + .../acid.zalan.do/v1/expansion_generated.go | 8 + .../acid.zalan.do/v1/operatorconfiguration.go | 76 ++ pkg/util/config/config.go | 1 - 20 files changed, 2601 insertions(+), 2188 deletions(-) create mode 100644 pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml create mode 100644 pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go create mode 100644 pkg/generated/listers/acid.zalan.do/v1/operatorconfiguration.go diff --git a/LICENSE b/LICENSE index 2141e8bcb..2f94edc31 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2025 Zalando SE +Copyright (c) 2026 Zalando SE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index c1becbc99..323b51892 100644 --- a/Makefile +++ b/Makefile @@ -66,15 +66,15 @@ $(GENERATED): go.mod $(CRD_SOURCES) $(GENERATED_CRDS): $(GENERATED) go tool controller-gen crd:crdVersions=v1,allowDangerousTypes=true paths=./pkg/apis/acid.zalan.do/... output:crd:dir=manifests - # only generate postgresteam.crd.yaml and postgresql.crd.yaml for now - @rm manifests/acid.zalan.do_operatorconfigurations.yaml @mv manifests/acid.zalan.do_postgresqls.yaml manifests/postgresql.crd.yaml @# hack to use lowercase kind and listKind @sed -i -e 's/kind: Postgresql/kind: postgresql/' manifests/postgresql.crd.yaml @sed -i -e 's/listKind: PostgresqlList/listKind: postgresqlList/' manifests/postgresql.crd.yaml @hack/adjust_postgresql_crd.sh + @mv manifests/acid.zalan.do_operatorconfigurations.yaml manifests/operatorconfiguration.crd.yaml @mv manifests/acid.zalan.do_postgresteams.yaml manifests/postgresteam.crd.yaml @cp manifests/postgresql.crd.yaml pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml + @cp manifests/operatorconfiguration.crd.yaml pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml local: ${SOURCES} $(GENERATED_CRDS) CGO_ENABLED=${CGO_ENABLED} go build -o build/${BINARY} $(LOCAL_BUILD_FLAGS) -ldflags "$(LDFLAGS)" $(SOURCES) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 1414366ff..5875b5808 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -7,281 +7,286 @@ metadata: spec: group: acid.zalan.do names: + categories: + - all kind: OperatorConfiguration listKind: OperatorConfigurationList plural: operatorconfigurations - singular: operatorconfiguration shortNames: - opconfig - categories: - - all + singular: operatorconfiguration scope: Namespaced versions: - - name: v1 - served: true - storage: true - subresources: - status: {} - additionalPrinterColumns: - - name: Image - type: string - description: Spilo image to be used for Pods + - additionalPrinterColumns: + - description: Spilo image to be used for Pods jsonPath: .configuration.docker_image - - name: Cluster-Label + name: Image type: string - description: Label for K8s resources created by operator + - description: Label for K8s resources created by operator jsonPath: .configuration.kubernetes.cluster_name_label - - name: Service-Account + name: Cluster-Label type: string - description: Name of service account to be used + - description: Name of service account to be used jsonPath: .configuration.kubernetes.pod_service_account_name - - name: Min-Instances - type: integer - description: Minimum number of instances per Postgres cluster + name: Service-Account + type: string + - description: Minimum number of instances per Postgres cluster jsonPath: .configuration.min_instances - - name: Age - type: date + name: Min-Instances + type: integer + - description: Age of the OperatorConfiguration resource jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: - type: object - required: - - kind - - apiVersion - - configuration + description: OperatorConfiguration defines the specification for the OperatorConfiguration. properties: - kind: - type: string - enum: - - OperatorConfiguration apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - enum: - - acid.zalan.do/v1 configuration: - type: object + description: OperatorConfigurationData defines the operation config properties: + aws_or_gcp: + description: AWSGCPConfiguration defines the configuration for AWS + properties: + additional_secret_mount: + type: string + additional_secret_mount_path: + type: string + aws_region: + default: eu-central-1 + type: string + enable_ebs_gp3_migration: + type: boolean + enable_ebs_gp3_migration_max_size: + format: int64 + type: integer + gcp_credentials: + type: string + kube_iam_role: + type: string + log_s3_bucket: + type: string + wal_az_storage_account: + type: string + wal_gs_bucket: + type: string + wal_s3_bucket: + type: string + type: object + connection_pooler: + description: ConnectionPoolerConfiguration defines default configuration + for connection pooler + properties: + connection_pooler_default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_image: + default: ghcr.io/zalando/postgres-operator/pgbouncer:latest + type: string + connection_pooler_max_db_connections: + format: int32 + type: integer + connection_pooler_mode: + default: transaction + enum: + - session + - transaction + type: string + connection_pooler_number_of_instances: + default: 2 + format: int32 + minimum: 1 + type: integer + connection_pooler_schema: + default: pooler + type: string + connection_pooler_user: + default: pooler + type: string + type: object crd_categories: - type: array - nullable: true items: type: string + type: array + debug: + description: OperatorDebugConfiguration defines options for the debug + mode + properties: + debug_logging: + default: true + type: boolean + enable_database_access: + default: true + type: boolean + type: object docker_image: + default: ghcr.io/zalando/spilo-18:4.1-p1 type: string - default: "ghcr.io/zalando/spilo-18:4.1-p1" enable_crd_registration: - type: boolean default: true - enable_crd_validation: type: boolean - description: deprecated - default: true enable_lazy_spilo_upgrade: type: boolean - default: false enable_maintenance_windows: - type: boolean default: true - enable_pgversion_env_var: type: boolean + enable_pgversion_env_var: default: true - enable_shm_volume: type: boolean + enable_shm_volume: default: true + type: boolean enable_spilo_wal_path_compat: type: boolean - default: false enable_team_id_clustername_prefix: type: boolean - default: false etcd_host: - type: string default: "" + type: string ignore_instance_limits_annotation_key: type: string ignore_resources_limits_annotation_key: type: string - kubernetes_use_configmaps: - type: boolean - default: false - maintenance_windows: - items: - 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))\ *$' - type: string - type: array - max_instances: - type: integer - description: "-1 = disabled" - minimum: -1 - default: -1 - min_instances: - type: integer - description: "-1 = disabled" - minimum: -1 - default: -1 - resync_period: - type: string - default: "30m" - repair_period: - type: string - default: "5m" - set_memory_request_to_limit: - type: boolean - default: false - sidecar_docker_images: - type: object - additionalProperties: - type: string - sidecars: - type: array - nullable: true - items: - type: object - x-kubernetes-preserve-unknown-fields: true - workers: - type: integer - minimum: 1 - default: 8 - users: - type: object - properties: - additional_owner_roles: - type: array - nullable: true - items: - type: string - enable_password_rotation: - type: boolean - default: false - password_rotation_interval: - type: integer - default: 90 - password_rotation_user_retention: - type: integer - default: 180 - replication_username: - type: string - default: standby - super_username: - type: string - default: postgres - major_version_upgrade: - type: object - properties: - major_version_upgrade_mode: - type: string - default: "manual" - major_version_upgrade_team_allow_list: - type: array - items: - type: string - minimal_major_version: - type: string - default: "14" - target_major_version: - type: string - default: "18" kubernetes: - type: object + description: KubernetesMetaConfiguration defines k8s conf required + for all Postgres clusters and the operator itself properties: additional_pod_capabilities: - type: array items: type: string + type: array cluster_domain: + default: cluster.local type: string - default: "cluster.local" cluster_labels: - type: object additionalProperties: type: string default: application: spilo + type: object cluster_name_label: + default: cluster-name type: string - default: "cluster-name" custom_pod_annotations: - type: object additionalProperties: type: string + type: object delete_annotation_date_key: type: string delete_annotation_name_key: type: string downscaler_annotations: - type: array items: type: string + type: array enable_cross_namespace_secret: type: boolean - default: false enable_finalizers: type: boolean - default: false enable_init_containers: - type: boolean default: true + type: boolean enable_owner_references: type: boolean - default: false enable_persistent_volume_claim_deletion: - type: boolean default: true + type: boolean enable_pod_antiaffinity: type: boolean - default: false enable_pod_disruption_budget: - type: boolean default: true + type: boolean enable_readiness_probe: type: boolean - default: false enable_secrets_deletion: - type: boolean default: true - enable_sidecars: type: boolean + enable_sidecars: default: true + type: boolean ignored_annotations: - type: array items: type: string + type: array infrastructure_roles_secret_name: - type: string + description: |- + NamespacedName comprises a resource name, with a mandatory namespace, + rendered as "/". Being a type captures intent and + helps make sure that UIDs, namespaced names and non-namespaced names + do not get conflated in code. For most use cases, namespace and name + will already have been format validated at the API entry point, so we + don't do that here. Where that's not the case (e.g. in testing), + consider using NamespacedNameOrDie() in testing.go in this package. + + from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object infrastructure_roles_secrets: - type: array - nullable: true + description: namespaced name of the secret containing infrastructure + roles names and passwords items: - type: object - required: - - secretname - - userkey - - passwordkey properties: - secretname: + defaultrolevalue: type: string - userkey: + defaultuservalue: + type: string + details: + description: This field point out the detailed yaml definition + of the role, if exists type: string passwordkey: type: string rolekey: type: string - defaultuservalue: - type: string - defaultrolevalue: - type: string - details: - type: string + secretname: + description: |- + Name of a secret which describes the role, and optionally name of a + configmap with an extra information + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object template: type: boolean - inherited_annotations: + userkey: + type: string + type: object type: array + inherited_annotations: items: type: string - inherited_labels: type: array + inherited_labels: items: type: string + type: array liveness_probe: description: |- Probe describes a health check to be performed against a container to determine whether it is @@ -336,11 +341,11 @@ spec: "Host" in httpHeaders instead. type: string httpHeaders: - description: Custom headers to set in the request. HTTP allows - repeated headers. + description: Custom headers to set in the request. HTTP + allows repeated headers. items: - description: HTTPHeader describes a custom header to be - used in HTTP probes + description: HTTPHeader describes a custom header to + be used in HTTP probes properties: name: description: |- @@ -436,274 +441,236 @@ spec: type: integer type: object master_pod_move_timeout: - type: string - default: "20m" + default: 20m + description: timeout for successful migration of master pods from + unschedulable node + format: int64 + type: integer node_readiness_label: - type: object additionalProperties: type: string + type: object node_readiness_label_merge: - type: string enum: - - "AND" - - "OR" - oauth_token_secret_name: + - AND + - OR type: string - default: "postgresql-operator" + oauth_token_secret_name: + default: postgres-operator + description: namespaced name of the secret containing the OAuth2 + token to pass to the teams API + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object pdb_master_label_selector: - type: boolean default: true + type: boolean pdb_name_format: + default: postgres-{cluster}-pdb + description: defines the template for PDB names type: string - default: "postgres-{cluster}-pdb" persistent_volume_claim_retention_policy: + additionalProperties: + type: string type: object - properties: - when_deleted: - type: string - enum: - - "delete" - - "retain" - when_scaled: - type: string - enum: - - "delete" - - "retain" pod_antiaffinity_preferred_during_scheduling: type: boolean - default: false pod_antiaffinity_topology_key: + default: kubernetes.io/hostname type: string - default: "kubernetes.io/hostname" pod_environment_configmap: - type: string + description: namespaced name of the ConfigMap with environment + variables to populate on every pod + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object pod_environment_secret: type: string pod_management_policy: - type: string + default: ordered_ready enum: - - "ordered_ready" - - "parallel" - default: "ordered_ready" + - ordered_ready + - parallel + type: string pod_priority_class_name: type: string pod_role_label: + default: spilo-role type: string - default: "spilo-role" pod_service_account_definition: type: string - default: "" pod_service_account_name: + default: postgres-pod type: string - default: "postgres-pod" pod_service_account_role_binding_definition: type: string - default: "" pod_terminate_grace_period: - type: string - default: "5m" + default: 5m + description: Postgres pods are terminated forcefully after this + timeout + format: int64 + type: integer secret_name_template: + default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' + description: |- + template for database user secrets generated by the operator, + here username contains the namespace in the format namespace.username + if the user is in different namespace than cluster and cross namespace secrets + are enabled via `enable_cross_namespace_secret` flag in the configuration. type: string - default: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" share_pgsocket_with_sidecars: type: boolean - default: false spilo_allow_privilege_escalation: - type: boolean default: true - spilo_runasuser: - type: integer - spilo_runasgroup: - type: integer + type: boolean spilo_fsgroup: + format: int64 type: integer spilo_privileged: type: boolean - default: false + spilo_runasgroup: + format: int64 + type: integer + spilo_runasuser: + format: int64 + type: integer storage_resize_mode: - type: string + default: pvc enum: - - "ebs" - - "mixed" - - "pvc" - - "off" - default: "pvc" + - ebs + - mixed + - pvc + - "off" + type: string toleration: - type: object additionalProperties: type: string + type: object watched_namespace: type: string - postgres_pod_resources: - type: object - properties: - default_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - default_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - default_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - default_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - max_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - max_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - min_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - min_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - timeouts: - type: object - properties: - patroni_api_check_interval: - type: string - default: "1s" - patroni_api_check_timeout: - type: string - default: "5s" - pod_label_wait_timeout: - type: string - default: "10m" - pod_deletion_wait_timeout: - type: string - default: "10m" - ready_wait_interval: - type: string - default: "4s" - ready_wait_timeout: - type: string - default: "30s" - resource_check_interval: - type: string - default: "3s" - resource_check_timeout: - type: string - default: "10m" - load_balancer: type: object + kubernetes_use_configmaps: + default: true + type: boolean + load_balancer: + description: LoadBalancerConfiguration defines the LB configuration properties: custom_service_annotations: - type: object additionalProperties: type: string + type: object db_hosted_zone: type: string - default: "db.example.com" enable_master_load_balancer: type: boolean - default: true - enable_master_pooler_load_balancer: - type: boolean - default: false - enable_replica_load_balancer: - type: boolean - default: false - enable_replica_pooler_load_balancer: - type: boolean - default: false enable_master_node_port: type: boolean - default: false + enable_master_pooler_load_balancer: + type: boolean enable_master_pooler_node_port: type: boolean - default: false + enable_replica_load_balancer: + type: boolean enable_replica_node_port: type: boolean - default: false + enable_replica_pooler_load_balancer: + type: boolean enable_replica_pooler_node_port: type: boolean - default: false external_traffic_policy: - type: string + default: Cluster enum: - - "Cluster" - - "Local" - default: "Cluster" + - Cluster + - Local + type: string master_dns_name_format: + default: '{cluster}.{namespace}.{hostedzone}' + description: defines the DNS name string template for the master + load balancer cluster type: string - default: "{cluster}.{namespace}.{hostedzone}" master_legacy_dns_name_format: + default: '{cluster}.{team}.{hostedzone}' + description: deprecated DNS template for master load balancer + using team name type: string - default: "{cluster}.{team}.{hostedzone}" replica_dns_name_format: + default: '{cluster}-repl.{namespace}.{hostedzone}' + description: defines the DNS name string template for the replica + load balancer cluster type: string - default: "{cluster}-repl.{namespace}.{hostedzone}" replica_legacy_dns_name_format: + default: '{cluster}-repl.{team}.{hostedzone}' + description: deprecated DNS template for replica load balancer + using team name type: string - default: "{cluster}-repl.{team}.{hostedzone}" - aws_or_gcp: type: object + logging_rest_api: + description: LoggingRESTAPIConfiguration defines Logging API conf properties: - additional_secret_mount: - type: string - additional_secret_mount_path: - type: string - aws_region: - type: string - default: "eu-central-1" - enable_ebs_gp3_migration: - type: boolean - default: false - enable_ebs_gp3_migration_max_size: + api_port: + default: 8080 type: integer + cluster_history_entries: default: 1000 - gcp_credentials: - type: string - kube_iam_role: - type: string - log_s3_bucket: - type: string - wal_az_storage_account: - type: string - wal_gs_bucket: - type: string - wal_s3_bucket: - type: string - logical_backup: + type: integer + ring_log_lines: + default: 100 + type: integer type: object + logical_backup: + description: OperatorLogicalBackupConfiguration defines configuration + for logical backup properties: + logical_backup_azure_storage_account_key: + type: string logical_backup_azure_storage_account_name: type: string logical_backup_azure_storage_container: type: string - logical_backup_azure_storage_account_key: - type: string logical_backup_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + logical_backup_cronjob_environment_secret: type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_docker_image: + default: ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1 type: string - default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + logical_backup_failed_jobs_history_limit: + default: 3 + format: int32 + minimum: 0 + type: integer logical_backup_google_application_credentials: type: string logical_backup_job_prefix: + default: logical-backup- type: string - default: "logical-backup-" logical_backup_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' logical_backup_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' logical_backup_provider: - type: string + default: s3 enum: - - "az" - - "gcs" - - "s3" - default: "s3" + - az + - gcs + - s3 + type: string logical_backup_s3_access_key_id: type: string logical_backup_s3_bucket: @@ -714,174 +681,290 @@ spec: type: string logical_backup_s3_region: type: string + logical_backup_s3_retention_time: + type: string logical_backup_s3_secret_access_key: type: string logical_backup_s3_sse: type: string - logical_backup_s3_retention_time: - type: string logical_backup_schedule: + default: 30 00 * * * + pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$ type: string - pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' - default: "30 00 * * *" - logical_backup_cronjob_environment_secret: - type: string - logical_backup_failed_jobs_history_limit: - type: integer - minimum: 0 - default: 3 logical_backup_successful_jobs_history_limit: - type: integer - minimum: 0 default: 3 - logical_backup_ttl_seconds_after_finished: - type: integer + format: int32 minimum: 0 + type: integer + logical_backup_ttl_seconds_after_finished: default: 86400 - debug: + format: int32 + minimum: 0 + type: integer type: object + maintenance_windows: + type: array + major_version_upgrade: + description: MajorVersionUpgradeConfiguration defines how to execute + major version upgrades of Postgres. properties: - debug_logging: - type: boolean - default: true - enable_database_access: + major_version_upgrade_mode: + default: manual + enum: + - "off" + - manual + - full + type: string + major_version_upgrade_team_allow_list: + items: + type: string + type: array + minimal_major_version: + default: "14" + type: string + target_major_version: + default: "18" + type: string + type: object + max_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + min_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + patroni: + description: PatroniConfiguration defines configuration for Patroni + properties: + enable_patroni_failsafe_mode: type: boolean - default: true - teams_api: type: object + postgres_pod_resources: + description: PostgresPodResourcesDefaults defines the spec of default + resources + properties: + default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + max_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + max_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + min_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + min_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + type: object + repair_period: + default: 5m + description: period between consecutive repair requests + format: int64 + type: integer + resync_period: + default: 30m + description: period between consecutive sync requests + format: int64 + type: integer + scalyr: + description: ScalyrConfiguration defines the configuration for ScalyrAPI + properties: + scalyr_api_key: + type: string + scalyr_cpu_limit: + default: "1" + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_cpu_request: + default: 100m + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_image: + type: string + scalyr_memory_limit: + default: 500Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_memory_request: + default: 50Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_server_url: + default: https://upload.eu.scalyr.com + type: string + type: object + set_memory_request_to_limit: + type: boolean + sidecar_docker_images: + additionalProperties: + type: string + type: object + sidecars: + type: object + x-kubernetes-preserve-unknown-fields: true + teams_api: + description: TeamsAPIConfiguration defines the configuration of TeamsAPI properties: enable_admin_role_for_users: - type: boolean default: true - enable_postgres_team_crd: type: boolean + enable_postgres_team_crd: default: true + type: boolean enable_postgres_team_crd_superusers: type: boolean - default: false enable_team_member_deprecation: type: boolean - default: false enable_team_superuser: type: boolean - default: false enable_teams_api: type: boolean - default: true pam_configuration: + default: https://info.example.com/oauth2/tokeninfo?access_token= + uid realm=/employees type: string - default: "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees" pam_role_name: + default: zalandos type: string - default: "zalandos" postgres_superuser_teams: - type: array items: type: string - protected_role_names: type: array + protected_role_names: + default: '["admin", "cron_admin"]' items: type: string - default: - - admin - - cron_admin + type: array role_deletion_suffix: + default: _deleted type: string - default: "_deleted" team_admin_role: + default: admin type: string - default: "admin" team_api_role_configuration: - type: object additionalProperties: type: string default: log_statement: all + type: object teams_api_url: + default: https://teams.example.com/api/ type: string - default: "https://teams.example.com/api/" - logging_rest_api: type: object + timeouts: + description: OperatorTimeouts defines the timeout of ResourceCheck, + PodWait, ReadyWait properties: - api_port: + patroni_api_check_interval: + default: 1s + description: interval between consecutive attempts of operator + calling the Patroni API + format: int64 type: integer - default: 8080 - cluster_history_entries: + patroni_api_check_timeout: + default: 5s + description: timeout when waiting for successful response from + Patroni API + format: int64 type: integer - default: 1000 - ring_log_lines: + pod_deletion_wait_timeout: + default: 10m + description: timeout when waiting for the Postgres pods to be + deleted + format: int64 + type: integer + pod_label_wait_timeout: + default: 10m + description: timeout when waiting for pod role and cluster labels + format: int64 + type: integer + ready_wait_interval: + default: 4s + description: interval between consecutive attempts waiting for + postgresql CRD to be created + format: int64 + type: integer + ready_wait_timeout: + default: 30s + description: timeout for the complete postgres CRD creation + format: int64 + type: integer + resource_check_interval: + default: 3s + description: interval to wait between consecutive attempts to + check for some K8s resources + format: int64 + type: integer + resource_check_timeout: + default: 10m + description: timeout when waiting for the presence of a certain + K8s resource + format: int64 type: integer - default: 100 - scalyr: # deprecated - type: object - properties: - scalyr_api_key: - type: string - scalyr_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - default: "1" - scalyr_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - default: "100m" - scalyr_image: - type: string - scalyr_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - default: "500Mi" - scalyr_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - default: "50Mi" - scalyr_server_url: - type: string - default: "https://upload.eu.scalyr.com" - connection_pooler: type: object + users: + description: PostgresUsersConfiguration defines the system users of + Postgres. properties: - connection_pooler_schema: - type: string - default: "pooler" - connection_pooler_user: - type: string - default: "pooler" - connection_pooler_image: - type: string - default: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" - connection_pooler_max_db_connections: + additional_owner_roles: + items: + type: string + type: array + enable_password_rotation: + type: boolean + password_rotation_interval: + default: 90 + format: int32 type: integer - default: 60 - connection_pooler_mode: - type: string - enum: - - "session" - - "transaction" - default: "transaction" - connection_pooler_number_of_instances: + password_rotation_user_retention: + default: 120 + format: int32 type: integer - minimum: 1 - default: 2 - connection_pooler_default_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - connection_pooler_default_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - connection_pooler_default_memory_limit: + replication_username: + default: standby type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - connection_pooler_default_memory_request: + super_username: + default: postgres type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - patroni: type: object - properties: - enable_patroni_failsafe_mode: - type: boolean - default: false - status: + workers: + default: 8 + format: int32 + minimum: 1 + type: integer type: object - additionalProperties: - type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + required: + - configuration + - metadata + type: object + served: true + storage: true + subresources: + status: {} diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index d3d1fa742..13d29c950 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -79,11 +79,6 @@ Those are top-level keys, containing both leaf keys and groups. Instruct the operator to create/update the CRDs. If disabled the operator will rely on the CRDs being managed separately. The default is `true`. -* **enable_crd_validation** - *deprecated*: toggles if the operator will create or update CRDs with - [OpenAPI v3 schema validation](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#validation) - The default is `true`. `false` will be ignored, since `apiextensions.io/v1` requires a structural schema definition. - * **crd_categories** The operator will register CRDs in the `all` category by default so that they will be returned by a `kubectl get all` call. You are free to change categories or leave them empty. diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 1096e0265..1c663c757 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -38,7 +38,6 @@ data: # downscaler_annotations: "deployment-time,downscaler/*" 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" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index fb009c459..5f347f2ac 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -1,285 +1,293 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 name: operatorconfigurations.acid.zalan.do spec: group: acid.zalan.do names: + categories: + - all kind: OperatorConfiguration listKind: OperatorConfigurationList plural: operatorconfigurations - singular: operatorconfiguration shortNames: - opconfig - categories: - - all + singular: operatorconfiguration scope: Namespaced versions: - - name: v1 - served: true - storage: true - subresources: - status: {} - additionalPrinterColumns: - - name: Image - type: string - description: Spilo image to be used for Pods + - additionalPrinterColumns: + - description: Spilo image to be used for Pods jsonPath: .configuration.docker_image - - name: Cluster-Label + name: Image type: string - description: Label for K8s resources created by operator + - description: Label for K8s resources created by operator jsonPath: .configuration.kubernetes.cluster_name_label - - name: Service-Account + name: Cluster-Label type: string - description: Name of service account to be used + - description: Name of service account to be used jsonPath: .configuration.kubernetes.pod_service_account_name - - name: Min-Instances - type: integer - description: Minimum number of instances per Postgres cluster + name: Service-Account + type: string + - description: Minimum number of instances per Postgres cluster jsonPath: .configuration.min_instances - - name: Age - type: date + name: Min-Instances + type: integer + - description: Age of the OperatorConfiguration resource jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: - type: object - required: - - kind - - apiVersion - - configuration + description: OperatorConfiguration defines the specification for the OperatorConfiguration. properties: - kind: - type: string - enum: - - OperatorConfiguration apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - enum: - - acid.zalan.do/v1 configuration: - type: object + description: OperatorConfigurationData defines the operation config properties: + aws_or_gcp: + description: AWSGCPConfiguration defines the configuration for AWS + properties: + additional_secret_mount: + type: string + additional_secret_mount_path: + type: string + aws_region: + default: eu-central-1 + type: string + enable_ebs_gp3_migration: + type: boolean + enable_ebs_gp3_migration_max_size: + format: int64 + type: integer + gcp_credentials: + type: string + kube_iam_role: + type: string + log_s3_bucket: + type: string + wal_az_storage_account: + type: string + wal_gs_bucket: + type: string + wal_s3_bucket: + type: string + type: object + connection_pooler: + description: ConnectionPoolerConfiguration defines default configuration + for connection pooler + properties: + connection_pooler_default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_image: + default: ghcr.io/zalando/postgres-operator/pgbouncer:latest + type: string + connection_pooler_max_db_connections: + format: int32 + type: integer + connection_pooler_mode: + default: transaction + enum: + - session + - transaction + type: string + connection_pooler_number_of_instances: + default: 2 + format: int32 + minimum: 1 + type: integer + connection_pooler_schema: + default: pooler + type: string + connection_pooler_user: + default: pooler + type: string + type: object crd_categories: - type: array - nullable: true items: type: string + type: array + debug: + description: OperatorDebugConfiguration defines options for the debug + mode + properties: + debug_logging: + default: true + type: boolean + enable_database_access: + default: true + type: boolean + type: object docker_image: + default: ghcr.io/zalando/spilo-18:4.1-p1 type: string - default: "ghcr.io/zalando/spilo-18:4.1-p1" enable_crd_registration: - type: boolean default: true - enable_crd_validation: type: boolean - description: deprecated - default: true enable_lazy_spilo_upgrade: type: boolean - default: false enable_maintenance_windows: - type: boolean default: true - enable_pgversion_env_var: type: boolean + enable_pgversion_env_var: default: true - enable_shm_volume: type: boolean + enable_shm_volume: default: true + type: boolean enable_spilo_wal_path_compat: type: boolean - default: false enable_team_id_clustername_prefix: type: boolean - default: false etcd_host: - type: string default: "" + type: string ignore_instance_limits_annotation_key: type: string ignore_resources_limits_annotation_key: type: string - kubernetes_use_configmaps: - type: boolean - default: false - maintenance_windows: - items: - 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))\ *$' - type: string - type: array - max_instances: - type: integer - description: "-1 = disabled" - minimum: -1 - default: -1 - min_instances: - type: integer - description: "-1 = disabled" - minimum: -1 - default: -1 - resync_period: - type: string - default: "30m" - repair_period: - type: string - default: "5m" - set_memory_request_to_limit: - type: boolean - default: false - sidecar_docker_images: - type: object - additionalProperties: - type: string - sidecars: - type: array - nullable: true - items: - type: object - x-kubernetes-preserve-unknown-fields: true - workers: - type: integer - minimum: 1 - default: 8 - users: - type: object - properties: - additional_owner_roles: - type: array - nullable: true - items: - type: string - enable_password_rotation: - type: boolean - default: false - password_rotation_interval: - type: integer - default: 90 - password_rotation_user_retention: - type: integer - default: 180 - replication_username: - type: string - default: standby - super_username: - type: string - default: postgres - major_version_upgrade: - type: object - properties: - major_version_upgrade_mode: - type: string - default: "manual" - major_version_upgrade_team_allow_list: - type: array - items: - type: string - minimal_major_version: - type: string - default: "14" - target_major_version: - type: string - default: "18" kubernetes: - type: object + description: KubernetesMetaConfiguration defines k8s conf required + for all Postgres clusters and the operator itself properties: additional_pod_capabilities: - type: array items: type: string + type: array cluster_domain: + default: cluster.local type: string - default: "cluster.local" cluster_labels: - type: object additionalProperties: type: string default: application: spilo + type: object cluster_name_label: + default: cluster-name type: string - default: "cluster-name" custom_pod_annotations: - type: object additionalProperties: type: string + type: object delete_annotation_date_key: type: string delete_annotation_name_key: type: string downscaler_annotations: - type: array items: type: string + type: array enable_cross_namespace_secret: type: boolean - default: false enable_finalizers: type: boolean - default: false enable_init_containers: - type: boolean default: true + type: boolean enable_owner_references: type: boolean - default: false enable_persistent_volume_claim_deletion: - type: boolean default: true + type: boolean enable_pod_antiaffinity: type: boolean - default: false enable_pod_disruption_budget: - type: boolean default: true + type: boolean enable_readiness_probe: type: boolean - default: false enable_secrets_deletion: - type: boolean default: true - enable_sidecars: type: boolean + enable_sidecars: default: true + type: boolean ignored_annotations: - type: array items: type: string + type: array infrastructure_roles_secret_name: - type: string + description: |- + NamespacedName comprises a resource name, with a mandatory namespace, + rendered as "/". Being a type captures intent and + helps make sure that UIDs, namespaced names and non-namespaced names + do not get conflated in code. For most use cases, namespace and name + will already have been format validated at the API entry point, so we + don't do that here. Where that's not the case (e.g. in testing), + consider using NamespacedNameOrDie() in testing.go in this package. + + from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object infrastructure_roles_secrets: - type: array - nullable: true + description: namespaced name of the secret containing infrastructure + roles names and passwords items: - type: object - required: - - secretname - - userkey - - passwordkey properties: - secretname: + defaultrolevalue: type: string - userkey: + defaultuservalue: + type: string + details: + description: This field point out the detailed yaml definition + of the role, if exists type: string passwordkey: type: string rolekey: type: string - defaultuservalue: - type: string - defaultrolevalue: - type: string - details: - type: string + secretname: + description: |- + Name of a secret which describes the role, and optionally name of a + configmap with an extra information + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object template: type: boolean - inherited_annotations: + userkey: + type: string + type: object type: array + inherited_annotations: items: type: string - inherited_labels: type: array + inherited_labels: items: type: string + type: array liveness_probe: description: |- Probe describes a health check to be performed against a container to determine whether it is @@ -334,11 +342,11 @@ spec: "Host" in httpHeaders instead. type: string httpHeaders: - description: Custom headers to set in the request. HTTP allows - repeated headers. + description: Custom headers to set in the request. HTTP + allows repeated headers. items: - description: HTTPHeader describes a custom header to be - used in HTTP probes + description: HTTPHeader describes a custom header to + be used in HTTP probes properties: name: description: |- @@ -434,262 +442,236 @@ spec: type: integer type: object master_pod_move_timeout: - type: string - default: "20m" + default: 20m + description: timeout for successful migration of master pods from + unschedulable node + format: int64 + type: integer node_readiness_label: - type: object additionalProperties: type: string + type: object node_readiness_label_merge: - type: string enum: - - "AND" - - "OR" - oauth_token_secret_name: + - AND + - OR type: string - default: "postgresql-operator" + oauth_token_secret_name: + default: postgres-operator + description: namespaced name of the secret containing the OAuth2 + token to pass to the teams API + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object pdb_master_label_selector: - type: boolean default: true + type: boolean pdb_name_format: + default: postgres-{cluster}-pdb + description: defines the template for PDB names type: string - default: "postgres-{cluster}-pdb" persistent_volume_claim_retention_policy: + additionalProperties: + type: string type: object - properties: - when_deleted: - type: string - enum: - - "delete" - - "retain" - when_scaled: - type: string - enum: - - "delete" - - "retain" pod_antiaffinity_preferred_during_scheduling: type: boolean - default: false pod_antiaffinity_topology_key: + default: kubernetes.io/hostname type: string - default: "kubernetes.io/hostname" pod_environment_configmap: - type: string + description: namespaced name of the ConfigMap with environment + variables to populate on every pod + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object pod_environment_secret: type: string pod_management_policy: - type: string + default: ordered_ready enum: - - "ordered_ready" - - "parallel" - default: "ordered_ready" + - ordered_ready + - parallel + type: string pod_priority_class_name: type: string pod_role_label: + default: spilo-role type: string - default: "spilo-role" pod_service_account_definition: type: string - default: "" pod_service_account_name: + default: postgres-pod type: string - default: "postgres-pod" pod_service_account_role_binding_definition: type: string - default: "" pod_terminate_grace_period: - type: string - default: "5m" + default: 5m + description: Postgres pods are terminated forcefully after this + timeout + format: int64 + type: integer secret_name_template: + default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' + description: |- + template for database user secrets generated by the operator, + here username contains the namespace in the format namespace.username + if the user is in different namespace than cluster and cross namespace secrets + are enabled via `enable_cross_namespace_secret` flag in the configuration. type: string - default: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" share_pgsocket_with_sidecars: type: boolean - default: false spilo_allow_privilege_escalation: - type: boolean default: true - spilo_runasuser: - type: integer - spilo_runasgroup: - type: integer + type: boolean spilo_fsgroup: + format: int64 type: integer spilo_privileged: type: boolean - default: false + spilo_runasgroup: + format: int64 + type: integer + spilo_runasuser: + format: int64 + type: integer storage_resize_mode: - type: string + default: pvc enum: - - "ebs" - - "mixed" - - "pvc" - - "off" - default: "pvc" + - ebs + - mixed + - pvc + - "off" + type: string toleration: - type: object additionalProperties: type: string + type: object watched_namespace: type: string - postgres_pod_resources: - type: object - properties: - default_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - default_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - default_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - default_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - max_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - max_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - min_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - min_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - timeouts: - type: object - properties: - patroni_api_check_interval: - type: string - default: "1s" - patroni_api_check_timeout: - type: string - default: "5s" - pod_label_wait_timeout: - type: string - default: "10m" - pod_deletion_wait_timeout: - type: string - default: "10m" - ready_wait_interval: - type: string - default: "4s" - ready_wait_timeout: - type: string - default: "30s" - resource_check_interval: - type: string - default: "3s" - resource_check_timeout: - type: string - default: "10m" - load_balancer: type: object + kubernetes_use_configmaps: + default: true + type: boolean + load_balancer: + description: LoadBalancerConfiguration defines the LB configuration properties: custom_service_annotations: - type: object additionalProperties: type: string + type: object db_hosted_zone: type: string - default: "db.example.com" enable_master_load_balancer: type: boolean - default: true + enable_master_node_port: + type: boolean enable_master_pooler_load_balancer: type: boolean - default: false + enable_master_pooler_node_port: + type: boolean enable_replica_load_balancer: type: boolean - default: false + enable_replica_node_port: + type: boolean enable_replica_pooler_load_balancer: type: boolean - default: false + enable_replica_pooler_node_port: + type: boolean external_traffic_policy: - type: string + default: Cluster enum: - - "Cluster" - - "Local" - default: "Cluster" + - Cluster + - Local + type: string master_dns_name_format: + default: '{cluster}.{namespace}.{hostedzone}' + description: defines the DNS name string template for the master + load balancer cluster type: string - default: "{cluster}.{namespace}.{hostedzone}" master_legacy_dns_name_format: + default: '{cluster}.{team}.{hostedzone}' + description: deprecated DNS template for master load balancer + using team name type: string - default: "{cluster}.{team}.{hostedzone}" replica_dns_name_format: + default: '{cluster}-repl.{namespace}.{hostedzone}' + description: defines the DNS name string template for the replica + load balancer cluster type: string - default: "{cluster}-repl.{namespace}.{hostedzone}" replica_legacy_dns_name_format: + default: '{cluster}-repl.{team}.{hostedzone}' + description: deprecated DNS template for replica load balancer + using team name type: string - default: "{cluster}-repl.{team}.{hostedzone}" - aws_or_gcp: type: object + logging_rest_api: + description: LoggingRESTAPIConfiguration defines Logging API conf properties: - additional_secret_mount: - type: string - additional_secret_mount_path: - type: string - aws_region: - type: string - default: "eu-central-1" - enable_ebs_gp3_migration: - type: boolean - default: false - enable_ebs_gp3_migration_max_size: + api_port: + default: 8080 type: integer + cluster_history_entries: default: 1000 - gcp_credentials: - type: string - kube_iam_role: - type: string - log_s3_bucket: - type: string - wal_az_storage_account: - type: string - wal_gs_bucket: - type: string - wal_s3_bucket: - type: string - logical_backup: + type: integer + ring_log_lines: + default: 100 + type: integer type: object + logical_backup: + description: OperatorLogicalBackupConfiguration defines configuration + for logical backup properties: + logical_backup_azure_storage_account_key: + type: string logical_backup_azure_storage_account_name: type: string logical_backup_azure_storage_container: type: string - logical_backup_azure_storage_account_key: - type: string logical_backup_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + logical_backup_cronjob_environment_secret: type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_docker_image: + default: ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1 type: string - default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + logical_backup_failed_jobs_history_limit: + default: 3 + format: int32 + minimum: 0 + type: integer logical_backup_google_application_credentials: type: string logical_backup_job_prefix: + default: logical-backup- type: string - default: "logical-backup-" logical_backup_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' logical_backup_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' logical_backup_provider: - type: string + default: s3 enum: - - "az" - - "gcs" - - "s3" - default: "s3" + - az + - gcs + - s3 + type: string logical_backup_s3_access_key_id: type: string logical_backup_s3_bucket: @@ -700,174 +682,290 @@ spec: type: string logical_backup_s3_region: type: string + logical_backup_s3_retention_time: + type: string logical_backup_s3_secret_access_key: type: string logical_backup_s3_sse: type: string - logical_backup_s3_retention_time: - type: string logical_backup_schedule: + default: 30 00 * * * + pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$ type: string - pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' - default: "30 00 * * *" - logical_backup_cronjob_environment_secret: - type: string - logical_backup_failed_jobs_history_limit: - type: integer - minimum: 0 - default: 3 logical_backup_successful_jobs_history_limit: - type: integer - minimum: 0 default: 3 - logical_backup_ttl_seconds_after_finished: - type: integer + format: int32 minimum: 0 + type: integer + logical_backup_ttl_seconds_after_finished: default: 86400 - debug: + format: int32 + minimum: 0 + type: integer type: object + maintenance_windows: + type: array + major_version_upgrade: + description: MajorVersionUpgradeConfiguration defines how to execute + major version upgrades of Postgres. properties: - debug_logging: - type: boolean - default: true - enable_database_access: + major_version_upgrade_mode: + default: manual + enum: + - "off" + - manual + - full + type: string + major_version_upgrade_team_allow_list: + items: + type: string + type: array + minimal_major_version: + default: "14" + type: string + target_major_version: + default: "18" + type: string + type: object + max_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + min_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + patroni: + description: PatroniConfiguration defines configuration for Patroni + properties: + enable_patroni_failsafe_mode: type: boolean - default: true - teams_api: type: object + postgres_pod_resources: + description: PostgresPodResourcesDefaults defines the spec of default + resources + properties: + default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + max_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + max_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + min_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + min_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + type: object + repair_period: + default: 5m + description: period between consecutive repair requests + format: int64 + type: integer + resync_period: + default: 30m + description: period between consecutive sync requests + format: int64 + type: integer + scalyr: + description: ScalyrConfiguration defines the configuration for ScalyrAPI + properties: + scalyr_api_key: + type: string + scalyr_cpu_limit: + default: "1" + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_cpu_request: + default: 100m + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_image: + type: string + scalyr_memory_limit: + default: 500Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_memory_request: + default: 50Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_server_url: + default: https://upload.eu.scalyr.com + type: string + type: object + set_memory_request_to_limit: + type: boolean + sidecar_docker_images: + additionalProperties: + type: string + type: object + sidecars: + type: object + x-kubernetes-preserve-unknown-fields: true + teams_api: + description: TeamsAPIConfiguration defines the configuration of TeamsAPI properties: enable_admin_role_for_users: - type: boolean default: true - enable_postgres_team_crd: type: boolean + enable_postgres_team_crd: default: true + type: boolean enable_postgres_team_crd_superusers: type: boolean - default: false enable_team_member_deprecation: type: boolean - default: false enable_team_superuser: type: boolean - default: false enable_teams_api: type: boolean - default: true pam_configuration: + default: https://info.example.com/oauth2/tokeninfo?access_token= + uid realm=/employees type: string - default: "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees" pam_role_name: + default: zalandos type: string - default: "zalandos" postgres_superuser_teams: - type: array items: type: string - protected_role_names: type: array + protected_role_names: + default: '["admin", "cron_admin"]' items: type: string - default: - - admin - - cron_admin + type: array role_deletion_suffix: + default: _deleted type: string - default: "_deleted" team_admin_role: + default: admin type: string - default: "admin" team_api_role_configuration: - type: object additionalProperties: type: string default: log_statement: all + type: object teams_api_url: + default: https://teams.example.com/api/ type: string - default: "https://teams.example.com/api/" - logging_rest_api: type: object + timeouts: + description: OperatorTimeouts defines the timeout of ResourceCheck, + PodWait, ReadyWait properties: - api_port: + patroni_api_check_interval: + default: 1s + description: interval between consecutive attempts of operator + calling the Patroni API + format: int64 type: integer - default: 8080 - cluster_history_entries: + patroni_api_check_timeout: + default: 5s + description: timeout when waiting for successful response from + Patroni API + format: int64 type: integer - default: 1000 - ring_log_lines: + pod_deletion_wait_timeout: + default: 10m + description: timeout when waiting for the Postgres pods to be + deleted + format: int64 + type: integer + pod_label_wait_timeout: + default: 10m + description: timeout when waiting for pod role and cluster labels + format: int64 + type: integer + ready_wait_interval: + default: 4s + description: interval between consecutive attempts waiting for + postgresql CRD to be created + format: int64 + type: integer + ready_wait_timeout: + default: 30s + description: timeout for the complete postgres CRD creation + format: int64 + type: integer + resource_check_interval: + default: 3s + description: interval to wait between consecutive attempts to + check for some K8s resources + format: int64 + type: integer + resource_check_timeout: + default: 10m + description: timeout when waiting for the presence of a certain + K8s resource + format: int64 type: integer - default: 100 - scalyr: # deprecated - type: object - properties: - scalyr_api_key: - type: string - scalyr_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - default: "1" - scalyr_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - default: "100m" - scalyr_image: - type: string - scalyr_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - default: "500Mi" - scalyr_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - default: "50Mi" - scalyr_server_url: - type: string - default: "https://upload.eu.scalyr.com" - connection_pooler: type: object + users: + description: PostgresUsersConfiguration defines the system users of + Postgres. properties: - connection_pooler_schema: - type: string - default: "pooler" - connection_pooler_user: - type: string - default: "pooler" - connection_pooler_image: - type: string - default: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" - connection_pooler_max_db_connections: + additional_owner_roles: + items: + type: string + type: array + enable_password_rotation: + type: boolean + password_rotation_interval: + default: 90 + format: int32 type: integer - default: 60 - connection_pooler_mode: - type: string - enum: - - "session" - - "transaction" - default: "transaction" - connection_pooler_number_of_instances: + password_rotation_user_retention: + default: 120 + format: int32 type: integer - minimum: 1 - default: 2 - connection_pooler_default_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - connection_pooler_default_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - connection_pooler_default_memory_limit: + replication_username: + default: standby type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - connection_pooler_default_memory_request: + super_username: + default: postgres type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - patroni: type: object - properties: - enable_patroni_failsafe_mode: - type: boolean - default: false - status: + workers: + default: 8 + format: int32 + minimum: 1 + type: integer type: object - additionalProperties: - type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + required: + - configuration + - metadata + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 867f573b3..54aea7c1b 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -2,1132 +2,17 @@ package v1 import ( _ "embed" - "fmt" - acidzalando "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do" - "github.com/zalando/postgres-operator/pkg/util" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" ) // CRDResource* define names necesssary for the k8s CRD API const ( - PostgresCRDResourceKind = "postgresql" - - OperatorConfigCRDResouceKind = "OperatorConfiguration" - OperatorConfigCRDResourcePlural = "operatorconfigurations" - OperatorConfigCRDResourceList = OperatorConfigCRDResouceKind + "List" - OperatorConfigCRDResourceName = OperatorConfigCRDResourcePlural + "." + acidzalando.GroupName - OperatorConfigCRDResourceShort = "opconfig" + PostgresCRDResourceKind = "postgresql" + OperatorConfigCRDResourceKind = "OperatorConfiguration" ) -// OperatorConfigCRDResourceColumns definition of AdditionalPrinterColumns for OperatorConfiguration CRD -var OperatorConfigCRDResourceColumns = []apiextv1.CustomResourceColumnDefinition{ - { - Name: "Image", - Type: "string", - Description: "Spilo image to be used for Pods", - JSONPath: ".configuration.docker_image", - }, - { - Name: "Cluster-Label", - Type: "string", - Description: "Label for K8s resources created by operator", - JSONPath: ".configuration.kubernetes.cluster_name_label", - }, - { - Name: "Service-Account", - Type: "string", - Description: "Name of service account to be used", - JSONPath: ".configuration.kubernetes.pod_service_account_name", - }, - { - Name: "Min-Instances", - Type: "integer", - Description: "Minimum number of instances per Postgres cluster", - JSONPath: ".configuration.min_instances", - }, - { - Name: "Age", - Type: "date", - JSONPath: ".metadata.creationTimestamp", - }, -} - -var min1 = 1.0 -var minLength1 int64 = 1 -var minDisable = -1.0 - -// OperatorConfigCRDResourceValidation to check applied manifest parameters -var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextv1.JSONSchemaProps{ - Type: "object", - Required: []string{"kind", "apiVersion", "configuration"}, - Properties: map[string]apiextv1.JSONSchemaProps{ - "kind": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"OperatorConfiguration"`), - }, - }, - }, - "apiVersion": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"acid.zalan.do/v1"`), - }, - }, - }, - "configuration": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "crd_categories": { - Type: "array", - Nullable: true, - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "docker_image": { - Type: "string", - }, - "enable_crd_registration": { - Type: "boolean", - }, - "enable_crd_validation": { - Type: "boolean", - Description: "deprecated", - }, - "enable_lazy_spilo_upgrade": { - Type: "boolean", - }, - "enable_maintenance_windows": { - Type: "boolean", - }, - "enable_shm_volume": { - Type: "boolean", - }, - "enable_spilo_wal_path_compat": { - Type: "boolean", - Description: "deprecated", - }, - "enable_team_id_clustername_prefix": { - Type: "boolean", - }, - "etcd_host": { - Type: "string", - }, - "ignore_instance_limits_annotation_key": { - Type: "string", - }, - "ignore_resources_limits_annotation_key": { - Type: "string", - }, - "kubernetes_use_configmaps": { - Type: "boolean", - }, - "maintenance_windows": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - 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))\\ *$", - }, - }, - }, - "max_instances": { - Type: "integer", - Description: "-1 = disabled", - Minimum: &minDisable, - }, - "min_instances": { - Type: "integer", - Description: "-1 = disabled", - Minimum: &minDisable, - }, - "resync_period": { - Type: "string", - }, - "repair_period": { - Type: "string", - }, - "set_memory_request_to_limit": { - Type: "boolean", - }, - "sidecar_docker_images": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "sidecars": { - Type: "array", - Nullable: true, - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "object", - XPreserveUnknownFields: util.True(), - }, - }, - }, - "workers": { - Type: "integer", - Minimum: &min1, - }, - "users": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "additional_owner_roles": { - Type: "array", - Nullable: true, - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "enable_password_rotation": { - Type: "boolean", - }, - "password_rotation_interval": { - Type: "integer", - }, - "password_rotation_user_retention": { - Type: "integer", - }, - "replication_username": { - Type: "string", - }, - "super_username": { - Type: "string", - }, - }, - }, - "major_version_upgrade": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "major_version_upgrade_mode": { - Type: "string", - }, - "major_version_upgrade_team_allow_list": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "minimal_major_version": { - Type: "string", - }, - "target_major_version": { - Type: "string", - }, - }, - }, - "kubernetes": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "additional_pod_capabilities": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "cluster_domain": { - Type: "string", - }, - "cluster_labels": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "cluster_name_label": { - Type: "string", - }, - "custom_pod_annotations": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "delete_annotation_date_key": { - Type: "string", - }, - "delete_annotation_name_key": { - Type: "string", - }, - "downscaler_annotations": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "enable_cross_namespace_secret": { - Type: "boolean", - }, - "enable_finalizers": { - Type: "boolean", - }, - "enable_init_containers": { - Type: "boolean", - }, - "enable_owner_references": { - Type: "boolean", - }, - "enable_persistent_volume_claim_deletion": { - Type: "boolean", - }, - "enable_pod_antiaffinity": { - Type: "boolean", - }, - "enable_pod_disruption_budget": { - Type: "boolean", - }, - "enable_readiness_probe": { - Type: "boolean", - }, - "enable_secrets_deletion": { - Type: "boolean", - }, - "enable_sidecars": { - Type: "boolean", - }, - "ignored_annotations": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "infrastructure_roles_secret_name": { - Type: "string", - }, - "infrastructure_roles_secrets": { - Type: "array", - Nullable: true, - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "object", - Required: []string{"secretname", "userkey", "passwordkey"}, - Properties: map[string]apiextv1.JSONSchemaProps{ - "secretname": { - Type: "string", - }, - "userkey": { - Type: "string", - }, - "passwordkey": { - Type: "string", - }, - "rolekey": { - Type: "string", - }, - "defaultuservalue": { - Type: "string", - }, - "defaultrolevalue": { - Type: "string", - }, - "details": { - Type: "string", - }, - "template": { - Type: "boolean", - }, - }, - }, - }, - }, - "inherited_annotations": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "inherited_labels": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "liveness_probe": { - Description: "Periodic probe of container liveness. Container will be restarted if the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "exec": { - Description: "One and only one of the following should be specified. Exec specifies the action to take.", - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "command": { - Description: "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - }, - }, - "failureThreshold": { - Description: "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", - Type: "integer", - Format: "int32", - }, - "httpGet": { - Description: "HTTPGet specifies the http request to perform.", - Type: "object", - Required: []string{"port"}, - Properties: map[string]apiextv1.JSONSchemaProps{ - "host": { - Description: "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", - Type: "string", - }, - "httpHeaders": { - Description: "Custom headers to set in the request. HTTP allows repeated headers.", - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Description: "HTTPHeader describes a custom header to be used in HTTP probes", - Type: "object", - Required: []string{"name", "value"}, - Properties: map[string]apiextv1.JSONSchemaProps{ - "name": { - Description: "The header field name", - Type: "string", - }, - "value": { - Description: "The header field value", - Type: "string", - }, - }, - }, - }, - }, - "path": { - Description: "Path to access on the HTTP server.", - Type: "string", - }, - "port": { - Description: "Name or number of the port to access on the container. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.", - AnyOf: []apiextv1.JSONSchemaProps{ - { - Type: "integer", - }, - { - Type: "string", - }, - }, - XIntOrString: true, - }, - "scheme": { - Description: "Scheme to use for connecting to the host. Defaults to HTTP.", - Type: "string", - }, - }, - }, - "initialDelaySeconds": { - Description: "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", - Type: "integer", - Format: "int32", - }, - "periodSeconds": { - Description: "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", - Type: "integer", - Format: "int32", - }, - "successThreshold": { - Description: "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", - Type: "integer", - Format: "int32", - }, - "tcpSocket": { - Description: "TCPSocket specifies an action involving a TCP port. TCP hooks not yet supported TODO: implement a realistic TCP lifecycle hook", - Type: "object", - Required: []string{"port"}, - Properties: map[string]apiextv1.JSONSchemaProps{ - "host": { - Description: "Optional: Host name to connect to, defaults to the pod IP.", - Type: "string", - }, - "port": { - Description: "Number or name of the port to access on the container. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.", - XIntOrString: true, - AnyOf: []apiextv1.JSONSchemaProps{ - { - Type: "integer", - }, - { - Type: "string", - }, - }, - }, - }, - }, - "terminationGracePeriodSeconds": { - Description: "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", - Type: "integer", - Format: "int64", - }, - "timeoutSeconds": { - Description: "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", - Type: "integer", - Format: "int32", - }, - }, - }, - "master_pod_move_timeout": { - Type: "string", - }, - "node_readiness_label": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "node_readiness_label_merge": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"AND"`), - }, - { - Raw: []byte(`"OR"`), - }, - }, - }, - "oauth_token_secret_name": { - Type: "string", - }, - "pdb_name_format": { - Type: "string", - }, - "pdb_master_label_selector": { - Type: "boolean", - }, - "persistent_volume_claim_retention_policy": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "when_deleted": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"delete"`), - }, - { - Raw: []byte(`"retain"`), - }, - }, - }, - "when_scaled": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"delete"`), - }, - { - Raw: []byte(`"retain"`), - }, - }, - }, - }, - }, - "pod_antiaffinity_preferred_during_scheduling": { - Type: "boolean", - }, - "pod_antiaffinity_topology_key": { - Type: "string", - }, - "pod_environment_configmap": { - Type: "string", - }, - "pod_environment_secret": { - Type: "string", - }, - "pod_management_policy": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"ordered_ready"`), - }, - { - Raw: []byte(`"parallel"`), - }, - }, - }, - "pod_priority_class_name": { - Type: "string", - }, - "pod_role_label": { - Type: "string", - }, - "pod_service_account_definition": { - Type: "string", - }, - "pod_service_account_name": { - Type: "string", - }, - "pod_service_account_role_binding_definition": { - Type: "string", - }, - "pod_terminate_grace_period": { - Type: "string", - }, - "secret_name_template": { - Type: "string", - }, - "share_pgsocket_with_sidecars": { - Type: "boolean", - }, - "spilo_runasuser": { - Type: "integer", - }, - "spilo_runasgroup": { - Type: "integer", - }, - "spilo_fsgroup": { - Type: "integer", - }, - "spilo_privileged": { - Type: "boolean", - }, - "spilo_allow_privilege_escalation": { - Type: "boolean", - }, - "storage_resize_mode": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"ebs"`), - }, - { - Raw: []byte(`"mixed"`), - }, - { - Raw: []byte(`"pvc"`), - }, - { - Raw: []byte(`"off"`), - }, - }, - }, - "toleration": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "watched_namespace": { - Type: "string", - }, - }, - }, - "patroni": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "enable_patroni_failsafe_mode": { - Type: "boolean", - }, - }, - }, - "postgres_pod_resources": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "default_cpu_limit": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$|^$", - }, - "default_cpu_request": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$|^$", - }, - "default_memory_limit": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$|^$", - }, - "default_memory_request": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$|^$", - }, - "max_cpu_request": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$|^$", - }, - "max_memory_request": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$|^$", - }, - "min_cpu_limit": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$|^$", - }, - "min_memory_limit": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$|^$", - }, - }, - }, - "timeouts": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "patroni_api_check_interval": { - Type: "string", - }, - "patroni_api_check_timeout": { - Type: "string", - }, - "pod_label_wait_timeout": { - Type: "string", - }, - "pod_deletion_wait_timeout": { - Type: "string", - }, - "ready_wait_interval": { - Type: "string", - }, - "ready_wait_timeout": { - Type: "string", - }, - "resource_check_interval": { - Type: "string", - }, - "resource_check_timeout": { - Type: "string", - }, - }, - }, - "load_balancer": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "custom_service_annotations": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "db_hosted_zone": { - Type: "string", - }, - "enable_master_load_balancer": { - Type: "boolean", - }, - "enable_master_pooler_load_balancer": { - Type: "boolean", - }, - "enable_replica_load_balancer": { - Type: "boolean", - }, - "enable_replica_pooler_load_balancer": { - Type: "boolean", - }, - "enable_master_node_port": { - Type: "boolean", - }, - "enable_master_pooler_node_port": { - Type: "boolean", - }, - "enable_replica_node_port": { - Type: "boolean", - }, - "enable_replica_pooler_node_port": { - Type: "boolean", - }, - "external_traffic_policy": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"Cluster"`), - }, - { - Raw: []byte(`"Local"`), - }, - }, - }, - "master_dns_name_format": { - Type: "string", - }, - "master_legacy_dns_name_format": { - Type: "string", - }, - "replica_dns_name_format": { - Type: "string", - }, - "replica_legacy_dns_name_format": { - Type: "string", - }, - }, - }, - "aws_or_gcp": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "additional_secret_mount": { - Type: "string", - }, - "additional_secret_mount_path": { - Type: "string", - }, - "aws_region": { - Type: "string", - }, - "enable_ebs_gp3_migration": { - Type: "boolean", - }, - "enable_ebs_gp3_migration_max_size": { - Type: "integer", - }, - "gcp_credentials": { - Type: "string", - }, - "kube_iam_role": { - Type: "string", - }, - "log_s3_bucket": { - Type: "string", - }, - "wal_s3_bucket": { - Type: "string", - }, - }, - }, - "logical_backup": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "logical_backup_azure_storage_account_name": { - Type: "string", - }, - "logical_backup_azure_storage_container": { - Type: "string", - }, - "logical_backup_azure_storage_account_key": { - Type: "string", - }, - "logical_backup_cpu_limit": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", - }, - "logical_backup_cpu_request": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", - }, - "logical_backup_docker_image": { - Type: "string", - }, - "logical_backup_google_application_credentials": { - Type: "string", - }, - "logical_backup_job_prefix": { - Type: "string", - }, - "logical_backup_memory_limit": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", - }, - "logical_backup_memory_request": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", - }, - "logical_backup_provider": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"az"`), - }, - { - Raw: []byte(`"gcs"`), - }, - { - Raw: []byte(`"s3"`), - }, - }, - }, - "logical_backup_s3_access_key_id": { - Type: "string", - }, - "logical_backup_s3_bucket": { - Type: "string", - }, - "logical_backup_s3_bucket_prefix": { - Type: "string", - }, - "logical_backup_s3_endpoint": { - Type: "string", - }, - "logical_backup_s3_region": { - Type: "string", - }, - "logical_backup_s3_secret_access_key": { - Type: "string", - }, - "logical_backup_s3_sse": { - Type: "string", - }, - "logical_backup_s3_retention_time": { - Type: "string", - }, - "logical_backup_schedule": { - Type: "string", - Pattern: "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$", - }, - "logical_backup_cronjob_environment_secret": { - Type: "string", - }, - "logical_backup_successful_jobs_history_limit": { - Type: "integer", - }, - "logical_backup_failed_jobs_history_limit": { - Type: "integer", - }, - "logical_backup_ttl_seconds_after_finished": { - Type: "integer", - }, - }, - }, - "debug": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "debug_logging": { - Type: "boolean", - }, - "enable_database_access": { - Type: "boolean", - }, - }, - }, - "teams_api": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "enable_admin_role_for_users": { - Type: "boolean", - }, - "enable_postgres_team_crd": { - Type: "boolean", - }, - "enable_postgres_team_crd_superusers": { - Type: "boolean", - }, - "enable_team_member_deprecation": { - Type: "boolean", - }, - "enable_team_superuser": { - Type: "boolean", - }, - "enable_teams_api": { - Type: "boolean", - }, - "pam_configuration": { - Type: "string", - }, - "pam_role_name": { - Type: "string", - }, - "postgres_superuser_teams": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "protected_role_names": { - Type: "array", - Items: &apiextv1.JSONSchemaPropsOrArray{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "role_deletion_suffix": { - Type: "string", - }, - "team_admin_role": { - Type: "string", - }, - "team_api_role_configuration": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - "teams_api_url": { - Type: "string", - }, - }, - }, - "logging_rest_api": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "api_port": { - Type: "integer", - }, - "cluster_history_entries": { - Type: "integer", - }, - "ring_log_lines": { - Type: "integer", - }, - }, - }, - "scalyr": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "scalyr_api_key": { - Type: "string", - }, - "scalyr_cpu_limit": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", - }, - "scalyr_cpu_request": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", - }, - "scalyr_image": { - Type: "string", - }, - "scalyr_memory_limit": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", - }, - "scalyr_memory_request": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", - }, - "scalyr_server_url": { - Type: "string", - }, - }, - }, - "connection_pooler": { - Type: "object", - Properties: map[string]apiextv1.JSONSchemaProps{ - "connection_pooler_default_cpu_limit": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", - }, - "connection_pooler_default_cpu_request": { - Type: "string", - Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", - }, - "connection_pooler_default_memory_limit": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", - }, - "connection_pooler_default_memory_request": { - Type: "string", - Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", - }, - "connection_pooler_image": { - Type: "string", - }, - "connection_pooler_max_db_connections": { - Type: "integer", - }, - "connection_pooler_mode": { - Type: "string", - Enum: []apiextv1.JSON{ - { - Raw: []byte(`"session"`), - }, - { - Raw: []byte(`"transaction"`), - }, - }, - }, - "connection_pooler_number_of_instances": { - Type: "integer", - Minimum: &min1, - }, - "connection_pooler_schema": { - Type: "string", - }, - "connection_pooler_user": { - Type: "string", - }, - }, - }, - }, - }, - "status": { - Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ - Type: "string", - }, - }, - }, - }, - }, -} - -func buildCRD(name, kind, plural, list, short string, - categories []string, - columns []apiextv1.CustomResourceColumnDefinition, - validation apiextv1.CustomResourceValidation) *apiextv1.CustomResourceDefinition { - return &apiextv1.CustomResourceDefinition{ - TypeMeta: metav1.TypeMeta{ - APIVersion: fmt.Sprintf("%s/%s", apiextv1.GroupName, apiextv1.SchemeGroupVersion.Version), - Kind: "CustomResourceDefinition", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: apiextv1.CustomResourceDefinitionSpec{ - Group: SchemeGroupVersion.Group, - Names: apiextv1.CustomResourceDefinitionNames{ - Kind: kind, - ListKind: list, - Plural: plural, - Singular: kind, - ShortNames: []string{short}, - Categories: categories, - }, - Scope: apiextv1.NamespaceScoped, - Versions: []apiextv1.CustomResourceDefinitionVersion{ - { - Name: SchemeGroupVersion.Version, - Served: true, - Storage: true, - Subresources: &apiextv1.CustomResourceSubresources{ - Status: &apiextv1.CustomResourceSubresourceStatus{}, - }, - AdditionalPrinterColumns: columns, - Schema: &validation, - }, - }, - }, - } -} - //go:embed postgresql.crd.yaml var postgresqlCRDYAML []byte @@ -1144,14 +29,18 @@ func PostgresCRD(crdCategories []string) (*apiextv1.CustomResourceDefinition, er return &crd, nil } -// ConfigurationCRD returns CustomResourceDefinition built from OperatorConfigCRDResource -func ConfigurationCRD(crdCategories []string) *apiextv1.CustomResourceDefinition { - return buildCRD(OperatorConfigCRDResourceName, - OperatorConfigCRDResouceKind, - OperatorConfigCRDResourcePlural, - OperatorConfigCRDResourceList, - OperatorConfigCRDResourceShort, - crdCategories, - OperatorConfigCRDResourceColumns, - OperatorConfigCRDResourceValidation) +//go:embed operatorconfiguration.crd.yaml +var operatorConfigurationCRDYAML []byte + +// OperatorConfigurationCRD returns CustomResourceDefinition built from OperatorConfigurationCRDResource +func OperatorConfigurationCRD(crdCategories []string) (*apiextv1.CustomResourceDefinition, error) { + var crd apiextv1.CustomResourceDefinition + err := yaml.Unmarshal(operatorConfigurationCRDYAML, &crd) + if err != nil { + return nil, err + } + + crd.Spec.Names.Categories = crdCategories + + return &crd, nil } 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 5cec0ff33..60793c45c 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -13,11 +13,17 @@ import ( ) // +genclient -// +genclient:onlyVerbs=get -// +genclient:noStatus // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // OperatorConfiguration defines the specification for the OperatorConfiguration. +// +k8s:deepcopy-gen=true +// +kubebuilder:resource:categories=all,shortName=opconfig,scope=Namespaced +// +kubebuilder:printcolumn:name="Image",type=string,JSONPath=`.configuration.docker_image`,description="Spilo image to be used for Pods" +// +kubebuilder:printcolumn:name="Cluster-Label",type=string,JSONPath=`.configuration.kubernetes.cluster_name_label`,description="Label for K8s resources created by operator" +// +kubebuilder:printcolumn:name="Service-Account",type=string,JSONPath=`.configuration.kubernetes.pod_service_account_name`,description="Name of service account to be used" +// +kubebuilder:printcolumn:name="Min-Instances",type=integer,JSONPath=`.configuration.min_instances`,description="Minimum number of instances per Postgres cluster" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age of the OperatorConfiguration resource" +// +kubebuilder:subresource:status type OperatorConfiguration struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata"` @@ -37,102 +43,170 @@ type OperatorConfigurationList struct { // PostgresUsersConfiguration defines the system users of Postgres. type PostgresUsersConfiguration struct { - SuperUsername string `json:"super_username,omitempty"` - ReplicationUsername string `json:"replication_username,omitempty"` - AdditionalOwnerRoles []string `json:"additional_owner_roles,omitempty"` - EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"` - PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"` - PasswordRotationUserRetention uint32 `json:"password_rotation_user_retention,omitempty"` + // +kubebuilder:default=postgres + SuperUsername string `json:"super_username,omitempty"` + // +kubebuilder:default=standby + ReplicationUsername string `json:"replication_username,omitempty"` + AdditionalOwnerRoles []string `json:"additional_owner_roles,omitempty"` + EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"` + // +kubebuilder:default=90 + PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"` + // +kubebuilder:default=120 + PasswordRotationUserRetention uint32 `json:"password_rotation_user_retention,omitempty"` } // MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres. type MajorVersionUpgradeConfiguration struct { - MajorVersionUpgradeMode string `json:"major_version_upgrade_mode" default:"manual"` // off - no actions, manual - manifest triggers action, full - manifest and minimal version violation trigger upgrade + // +kubebuilder:validation:Enum=off;manual;full + // +kubebuilder:default=manual + MajorVersionUpgradeMode string `json:"major_version_upgrade_mode,omitempty"` // 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:"14"` - TargetMajorVersion string `json:"target_major_version" default:"18"` + // +kubebuilder:default="14" + MinimalMajorVersion string `json:"minimal_major_version,omitempty"` + // +kubebuilder:default="18" + TargetMajorVersion string `json:"target_major_version,omitempty"` } // KubernetesMetaConfiguration defines k8s conf required for all Postgres clusters and the operator itself type KubernetesMetaConfiguration struct { - EnableOwnerReferences *bool `json:"enable_owner_references,omitempty"` + EnableOwnerReferences *bool `json:"enable_owner_references,omitempty"` + // +kubebuilder:default=postgres-pod PodServiceAccountName string `json:"pod_service_account_name,omitempty"` // TODO: change it to the proper json - PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"` - PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"` - PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"` - LivenessProbe *v1.Probe `json:"liveness_probe"` - SpiloPrivileged bool `json:"spilo_privileged,omitempty"` - SpiloAllowPrivilegeEscalation *bool `json:"spilo_allow_privilege_escalation,omitempty"` - SpiloRunAsUser *int64 `json:"spilo_runasuser,omitempty"` - SpiloRunAsGroup *int64 `json:"spilo_runasgroup,omitempty"` - SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"` - AdditionalPodCapabilities []string `json:"additional_pod_capabilities,omitempty"` - WatchedNamespace string `json:"watched_namespace,omitempty"` - PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"` - PDBMasterLabelSelector *bool `json:"pdb_master_label_selector,omitempty"` - EnablePodDisruptionBudget *bool `json:"enable_pod_disruption_budget,omitempty"` - StorageResizeMode string `json:"storage_resize_mode,omitempty"` - EnableInitContainers *bool `json:"enable_init_containers,omitempty"` - EnableSidecars *bool `json:"enable_sidecars,omitempty"` - SharePgSocketWithSidecars *bool `json:"share_pgsocket_with_sidecars,omitempty"` - SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"` - ClusterDomain string `json:"cluster_domain,omitempty"` - OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"` - InfrastructureRolesSecretName spec.NamespacedName `json:"infrastructure_roles_secret_name,omitempty"` - InfrastructureRolesDefs []*config.InfrastructureRole `json:"infrastructure_roles_secrets,omitempty"` - PodRoleLabel string `json:"pod_role_label,omitempty"` - ClusterLabels map[string]string `json:"cluster_labels,omitempty"` - InheritedLabels []string `json:"inherited_labels,omitempty"` - InheritedAnnotations []string `json:"inherited_annotations,omitempty"` - DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"` - IgnoredAnnotations []string `json:"ignored_annotations,omitempty"` - ClusterNameLabel string `json:"cluster_name_label,omitempty"` - DeleteAnnotationDateKey string `json:"delete_annotation_date_key,omitempty"` - DeleteAnnotationNameKey string `json:"delete_annotation_name_key,omitempty"` - NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"` - NodeReadinessLabelMerge string `json:"node_readiness_label_merge,omitempty"` - CustomPodAnnotations map[string]string `json:"custom_pod_annotations,omitempty"` + PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"` + PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"` + // +kubebuilder:default="5m" + // Postgres pods are terminated forcefully after this timeout + PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"` + // +optional + LivenessProbe *v1.Probe `json:"liveness_probe"` + SpiloPrivileged bool `json:"spilo_privileged,omitempty"` + // +kubebuilder:default=true + SpiloAllowPrivilegeEscalation *bool `json:"spilo_allow_privilege_escalation,omitempty"` + SpiloRunAsUser *int64 `json:"spilo_runasuser,omitempty"` + SpiloRunAsGroup *int64 `json:"spilo_runasgroup,omitempty"` + SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"` + AdditionalPodCapabilities []string `json:"additional_pod_capabilities,omitempty"` + WatchedNamespace string `json:"watched_namespace,omitempty"` + // +kubebuilder:default="postgres-{cluster}-pdb" + // defines the template for PDB names + PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"` + // +kubebuilder:default=true + PDBMasterLabelSelector *bool `json:"pdb_master_label_selector,omitempty"` + // +kubebuilder:default=true + EnablePodDisruptionBudget *bool `json:"enable_pod_disruption_budget,omitempty"` + // +kubebuilder:validation:Enum=ebs;mixed;pvc;off + // +kubebuilder:default=pvc + StorageResizeMode string `json:"storage_resize_mode,omitempty"` + // +kubebuilder:default=true + EnableInitContainers *bool `json:"enable_init_containers,omitempty"` + // +kubebuilder:default=true + EnableSidecars *bool `json:"enable_sidecars,omitempty"` + SharePgSocketWithSidecars *bool `json:"share_pgsocket_with_sidecars,omitempty"` + // +kubebuilder:default="{username}.{cluster}.credentials.{tprkind}.{tprgroup}" + // template for database user secrets generated by the operator, + // here username contains the namespace in the format namespace.username + // if the user is in different namespace than cluster and cross namespace secrets + // are enabled via `enable_cross_namespace_secret` flag in the configuration. + SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"` + // +kubebuilder:default="cluster.local" + ClusterDomain string `json:"cluster_domain,omitempty"` + // +kubebuilder:default=postgres-operator + // namespaced name of the secret containing the OAuth2 token to pass to the teams API + OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"` + InfrastructureRolesSecretName spec.NamespacedName `json:"infrastructure_roles_secret_name,omitempty"` + // +kubebuilder:validation:Type=array + // namespaced name of the secret containing infrastructure roles names and passwords + InfrastructureRolesDefs []*config.InfrastructureRole `json:"infrastructure_roles_secrets,omitempty"` + // +kubebuilder:default=spilo-role + PodRoleLabel string `json:"pod_role_label,omitempty"` + // +kubebuilder:default={application: spilo} + ClusterLabels map[string]string `json:"cluster_labels,omitempty"` + InheritedLabels []string `json:"inherited_labels,omitempty"` + InheritedAnnotations []string `json:"inherited_annotations,omitempty"` + DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"` + IgnoredAnnotations []string `json:"ignored_annotations,omitempty"` + // +kubebuilder:default=cluster-name + ClusterNameLabel string `json:"cluster_name_label,omitempty"` + DeleteAnnotationDateKey string `json:"delete_annotation_date_key,omitempty"` + DeleteAnnotationNameKey string `json:"delete_annotation_name_key,omitempty"` + NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"` + // +kubebuilder:validation:Enum=AND;OR + NodeReadinessLabelMerge string `json:"node_readiness_label_merge,omitempty"` + CustomPodAnnotations map[string]string `json:"custom_pod_annotations,omitempty"` // TODO: use a proper toleration structure? - PodToleration map[string]string `json:"toleration,omitempty"` - PodEnvironmentConfigMap spec.NamespacedName `json:"pod_environment_configmap,omitempty"` - PodEnvironmentSecret string `json:"pod_environment_secret,omitempty"` - PodPriorityClassName string `json:"pod_priority_class_name,omitempty"` - MasterPodMoveTimeout Duration `json:"master_pod_move_timeout,omitempty"` - EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"` - PodAntiAffinityPreferredDuringScheduling bool `json:"pod_antiaffinity_preferred_during_scheduling,omitempty"` - PodAntiAffinityTopologyKey string `json:"pod_antiaffinity_topology_key,omitempty"` - PodManagementPolicy string `json:"pod_management_policy,omitempty"` - PersistentVolumeClaimRetentionPolicy map[string]string `json:"persistent_volume_claim_retention_policy,omitempty"` - EnableSecretsDeletion *bool `json:"enable_secrets_deletion,omitempty"` - EnablePersistentVolumeClaimDeletion *bool `json:"enable_persistent_volume_claim_deletion,omitempty"` - EnableReadinessProbe bool `json:"enable_readiness_probe,omitempty"` - EnableCrossNamespaceSecret bool `json:"enable_cross_namespace_secret,omitempty"` - EnableFinalizers *bool `json:"enable_finalizers,omitempty"` + PodToleration map[string]string `json:"toleration,omitempty"` + // namespaced name of the ConfigMap with environment variables to populate on every pod + PodEnvironmentConfigMap spec.NamespacedName `json:"pod_environment_configmap,omitempty"` + PodEnvironmentSecret string `json:"pod_environment_secret,omitempty"` + PodPriorityClassName string `json:"pod_priority_class_name,omitempty"` + // +kubebuilder:default="20m" + // timeout for successful migration of master pods from unschedulable node + MasterPodMoveTimeout Duration `json:"master_pod_move_timeout,omitempty"` + EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"` + PodAntiAffinityPreferredDuringScheduling bool `json:"pod_antiaffinity_preferred_during_scheduling,omitempty"` + // +kubebuilder:default="kubernetes.io/hostname" + PodAntiAffinityTopologyKey string `json:"pod_antiaffinity_topology_key,omitempty"` + // +kubebuilder:validation:Enum=ordered_ready;parallel + // +kubebuilder:default=ordered_ready + PodManagementPolicy string `json:"pod_management_policy,omitempty"` + PersistentVolumeClaimRetentionPolicy map[string]string `json:"persistent_volume_claim_retention_policy,omitempty"` + + // +kubebuilder:default=true + EnableSecretsDeletion *bool `json:"enable_secrets_deletion,omitempty"` + // +kubebuilder:default=true + EnablePersistentVolumeClaimDeletion *bool `json:"enable_persistent_volume_claim_deletion,omitempty"` + EnableReadinessProbe bool `json:"enable_readiness_probe,omitempty"` + EnableCrossNamespaceSecret bool `json:"enable_cross_namespace_secret,omitempty"` + EnableFinalizers *bool `json:"enable_finalizers,omitempty"` } // PostgresPodResourcesDefaults defines the spec of default resources type PostgresPodResourcesDefaults struct { - DefaultCPURequest string `json:"default_cpu_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + DefaultCPURequest string `json:"default_cpu_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` DefaultMemoryRequest string `json:"default_memory_request,omitempty"` - DefaultCPULimit string `json:"default_cpu_limit,omitempty"` - DefaultMemoryLimit string `json:"default_memory_limit,omitempty"` - MinCPULimit string `json:"min_cpu_limit,omitempty"` - MinMemoryLimit string `json:"min_memory_limit,omitempty"` - MaxCPURequest string `json:"max_cpu_request,omitempty"` - MaxMemoryRequest string `json:"max_memory_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + DefaultCPULimit string `json:"default_cpu_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + DefaultMemoryLimit string `json:"default_memory_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + MinCPULimit string `json:"min_cpu_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + MinMemoryLimit string `json:"min_memory_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + MaxCPURequest string `json:"max_cpu_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + MaxMemoryRequest string `json:"max_memory_request,omitempty"` } // OperatorTimeouts defines the timeout of ResourceCheck, PodWait, ReadyWait type OperatorTimeouts struct { - ResourceCheckInterval Duration `json:"resource_check_interval,omitempty"` - ResourceCheckTimeout Duration `json:"resource_check_timeout,omitempty"` - PodLabelWaitTimeout Duration `json:"pod_label_wait_timeout,omitempty"` - PodDeletionWaitTimeout Duration `json:"pod_deletion_wait_timeout,omitempty"` - ReadyWaitInterval Duration `json:"ready_wait_interval,omitempty"` - ReadyWaitTimeout Duration `json:"ready_wait_timeout,omitempty"` + // +kubebuilder:default="3s" + // interval to wait between consecutive attempts to check for some K8s resources + ResourceCheckInterval Duration `json:"resource_check_interval,omitempty"` + // +kubebuilder:default="10m" + // timeout when waiting for the presence of a certain K8s resource + ResourceCheckTimeout Duration `json:"resource_check_timeout,omitempty"` + // +kubebuilder:default="10m" + // timeout when waiting for pod role and cluster labels + PodLabelWaitTimeout Duration `json:"pod_label_wait_timeout,omitempty"` + // +kubebuilder:default="10m" + // timeout when waiting for the Postgres pods to be deleted + PodDeletionWaitTimeout Duration `json:"pod_deletion_wait_timeout,omitempty"` + // +kubebuilder:default="4s" + // interval between consecutive attempts waiting for postgresql CRD to be created + ReadyWaitInterval Duration `json:"ready_wait_interval,omitempty"` + // +kubebuilder:default="30s" + // timeout for the complete postgres CRD creation + ReadyWaitTimeout Duration `json:"ready_wait_timeout,omitempty"` + // +kubebuilder:default="1s" + // interval between consecutive attempts of operator calling the Patroni API PatroniAPICheckInterval Duration `json:"patroni_api_check_interval,omitempty"` - PatroniAPICheckTimeout Duration `json:"patroni_api_check_timeout,omitempty"` + // +kubebuilder:default="5s" + // timeout when waiting for successful response from Patroni API + PatroniAPICheckTimeout Duration `json:"patroni_api_check_timeout,omitempty"` } // LoadBalancerConfiguration defines the LB configuration @@ -143,24 +217,36 @@ type LoadBalancerConfiguration struct { EnableReplicaLoadBalancer bool `json:"enable_replica_load_balancer,omitempty"` EnableReplicaPoolerLoadBalancer bool `json:"enable_replica_pooler_load_balancer,omitempty"` - // kept in LoadBalancerConfiguration because all the other parameters apply here too + // NodePort flags kept in LoadBalancerConfiguration because all the other parameters apply here too + EnableMasterNodePort bool `json:"enable_master_node_port,omitempty"` EnableMasterPoolerNodePort bool `json:"enable_master_pooler_node_port,omitempty"` EnableReplicaNodePort bool `json:"enable_replica_node_port,omitempty"` EnableReplicaPoolerNodePort bool `json:"enable_replica_pooler_node_port,omitempty"` - CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"` - MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"` - MasterLegacyDNSNameFormat config.StringTemplate `json:"master_legacy_dns_name_format,omitempty"` - ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"` + CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"` + // +kubebuilder:default="{cluster}.{namespace}.{hostedzone}" + // defines the DNS name string template for the master load balancer cluster + MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"` + // +kubebuilder:default="{cluster}.{team}.{hostedzone}" + // deprecated DNS template for master load balancer using team name + MasterLegacyDNSNameFormat config.StringTemplate `json:"master_legacy_dns_name_format,omitempty"` + // +kubebuilder:default="{cluster}-repl.{namespace}.{hostedzone}" + // defines the DNS name string template for the replica load balancer cluster + ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"` + // +kubebuilder:default="{cluster}-repl.{team}.{hostedzone}" + // deprecated DNS template for replica load balancer using team name ReplicaLegacyDNSNameFormat config.StringTemplate `json:"replica_legacy_dns_name_format,omitempty"` - ExternalTrafficPolicy string `json:"external_traffic_policy" default:"Cluster"` + // +kubebuilder:validation:Enum=Cluster;Local + // +kubebuilder:default=Cluster + ExternalTrafficPolicy string `json:"external_traffic_policy,omitempty"` } // AWSGCPConfiguration defines the configuration for AWS // TODO complete Google Cloud Platform (GCP) configuration type AWSGCPConfiguration struct { - WALES3Bucket string `json:"wal_s3_bucket,omitempty"` + WALES3Bucket string `json:"wal_s3_bucket,omitempty"` + // +kubebuilder:default=eu-central-1 AWSRegion string `json:"aws_region,omitempty"` WALGSBucket string `json:"wal_gs_bucket,omitempty"` GCPCredentials string `json:"gcp_credentials,omitempty"` @@ -169,70 +255,109 @@ type AWSGCPConfiguration struct { KubeIAMRole string `json:"kube_iam_role,omitempty"` AdditionalSecretMount string `json:"additional_secret_mount,omitempty"` 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"` + EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration,omitempty"` + EnableEBSGp3MigrationMaxSize int64 `json:"enable_ebs_gp3_migration_max_size,omitempty"` } // OperatorDebugConfiguration defines options for the debug mode type OperatorDebugConfiguration struct { - DebugLogging *bool `json:"debug_logging,omitempty"` + // +kubebuilder:default=true + DebugLogging *bool `json:"debug_logging,omitempty"` + // +kubebuilder:default=true EnableDBAccess *bool `json:"enable_database_access,omitempty"` } // TeamsAPIConfiguration defines the configuration of TeamsAPI type TeamsAPIConfiguration struct { - EnableTeamsAPI bool `json:"enable_teams_api,omitempty"` - TeamsAPIUrl string `json:"teams_api_url,omitempty"` - TeamAPIRoleConfiguration map[string]string `json:"team_api_role_configuration,omitempty"` - EnableTeamSuperuser bool `json:"enable_team_superuser,omitempty"` - EnableAdminRoleForUsers bool `json:"enable_admin_role_for_users,omitempty"` - TeamAdminRole string `json:"team_admin_role,omitempty"` - PamRoleName string `json:"pam_role_name,omitempty"` - PamConfiguration string `json:"pam_configuration,omitempty"` - ProtectedRoles []string `json:"protected_role_names,omitempty"` - PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"` - EnablePostgresTeamCRD bool `json:"enable_postgres_team_crd,omitempty"` - EnablePostgresTeamCRDSuperusers bool `json:"enable_postgres_team_crd_superusers,omitempty"` - EnableTeamMemberDeprecation bool `json:"enable_team_member_deprecation,omitempty"` - RoleDeletionSuffix string `json:"role_deletion_suffix,omitempty"` + EnableTeamsAPI bool `json:"enable_teams_api,omitempty"` + // +kubebuilder:default="https://teams.example.com/api/" + TeamsAPIUrl string `json:"teams_api_url,omitempty"` + // +kubebuilder:default={log_statement: all} + TeamAPIRoleConfiguration map[string]string `json:"team_api_role_configuration,omitempty"` + EnableTeamSuperuser bool `json:"enable_team_superuser,omitempty"` + // +kubebuilder:default=true + EnableAdminRoleForUsers bool `json:"enable_admin_role_for_users,omitempty"` + // +kubebuilder:default=admin + TeamAdminRole string `json:"team_admin_role,omitempty"` + // +kubebuilder:default=zalandos + PamRoleName string `json:"pam_role_name,omitempty"` + // +kubebuilder:default="https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees" + PamConfiguration string `json:"pam_configuration,omitempty"` + // +kubebuilder:default="[\"admin\", \"cron_admin\"]" + ProtectedRoles []string `json:"protected_role_names,omitempty"` + PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"` + // +kubebuilder:default=true + EnablePostgresTeamCRD bool `json:"enable_postgres_team_crd,omitempty"` + EnablePostgresTeamCRDSuperusers bool `json:"enable_postgres_team_crd_superusers,omitempty"` + EnableTeamMemberDeprecation bool `json:"enable_team_member_deprecation,omitempty"` + // +kubebuilder:default=_deleted + RoleDeletionSuffix string `json:"role_deletion_suffix,omitempty"` } // LoggingRESTAPIConfiguration defines Logging API conf type LoggingRESTAPIConfiguration struct { - APIPort int `json:"api_port,omitempty"` - RingLogLines int `json:"ring_log_lines,omitempty"` + // +kubebuilder:default=8080 + APIPort int `json:"api_port,omitempty"` + // +kubebuilder:default=100 + RingLogLines int `json:"ring_log_lines,omitempty"` + // +kubebuilder:default=1000 ClusterHistoryEntries int `json:"cluster_history_entries,omitempty"` } // ScalyrConfiguration defines the configuration for ScalyrAPI type ScalyrConfiguration struct { - ScalyrAPIKey string `json:"scalyr_api_key,omitempty"` - ScalyrImage string `json:"scalyr_image,omitempty"` - ScalyrServerURL string `json:"scalyr_server_url,omitempty"` - ScalyrCPURequest string `json:"scalyr_cpu_request,omitempty"` + ScalyrAPIKey string `json:"scalyr_api_key,omitempty"` + ScalyrImage string `json:"scalyr_image,omitempty"` + // +kubebuilder:default="https://upload.eu.scalyr.com" + ScalyrServerURL string `json:"scalyr_server_url,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + // +kubebuilder:default="100m" + ScalyrCPURequest string `json:"scalyr_cpu_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + // +kubebuilder:default="50Mi" ScalyrMemoryRequest string `json:"scalyr_memory_request,omitempty"` - ScalyrCPULimit string `json:"scalyr_cpu_limit,omitempty"` - ScalyrMemoryLimit string `json:"scalyr_memory_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + // +kubebuilder:default="1" + ScalyrCPULimit string `json:"scalyr_cpu_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + // +kubebuilder:default="500Mi" + ScalyrMemoryLimit string `json:"scalyr_memory_limit,omitempty"` } // ConnectionPoolerConfiguration defines default configuration for connection pooler type ConnectionPoolerConfiguration struct { - NumberOfInstances *int32 `json:"connection_pooler_number_of_instances,omitempty"` - Schema string `json:"connection_pooler_schema,omitempty"` - User string `json:"connection_pooler_user,omitempty"` - Image string `json:"connection_pooler_image,omitempty"` - Mode string `json:"connection_pooler_mode,omitempty"` - MaxDBConnections *int32 `json:"connection_pooler_max_db_connections,omitempty"` - DefaultCPURequest string `json:"connection_pooler_default_cpu_request,omitempty"` + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:default=2 + NumberOfInstances *int32 `json:"connection_pooler_number_of_instances,omitempty"` + // +kubebuilder:default=pooler + Schema string `json:"connection_pooler_schema,omitempty"` + // +kubebuilder:default=pooler + User string `json:"connection_pooler_user,omitempty"` + // +kubebuilder:default="ghcr.io/zalando/postgres-operator/pgbouncer:latest" + Image string `json:"connection_pooler_image,omitempty"` + // +kubebuilder:validation:Enum=session;transaction + // +kubebuilder:default=transaction + Mode string `json:"connection_pooler_mode,omitempty"` + MaxDBConnections *int32 `json:"connection_pooler_max_db_connections,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + DefaultCPURequest string `json:"connection_pooler_default_cpu_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` DefaultMemoryRequest string `json:"connection_pooler_default_memory_request,omitempty"` - DefaultCPULimit string `json:"connection_pooler_default_cpu_limit,omitempty"` - DefaultMemoryLimit string `json:"connection_pooler_default_memory_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + DefaultCPULimit string `json:"connection_pooler_default_cpu_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + DefaultMemoryLimit string `json:"connection_pooler_default_memory_limit,omitempty"` } // OperatorLogicalBackupConfiguration defines configuration for logical backup type OperatorLogicalBackupConfiguration struct { - Schedule string `json:"logical_backup_schedule,omitempty"` - DockerImage string `json:"logical_backup_docker_image,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$` + // +kubebuilder:default="30 00 * * *" + Schedule string `json:"logical_backup_schedule,omitempty"` + // +kubebuilder:default="ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + DockerImage string `json:"logical_backup_docker_image,omitempty"` + // +kubebuilder:validation:Enum=az;gcs;s3 + // +kubebuilder:default=s3 BackupProvider string `json:"logical_backup_provider,omitempty"` AzureStorageAccountName string `json:"logical_backup_azure_storage_account_name,omitempty"` AzureStorageContainer string `json:"logical_backup_azure_storage_container,omitempty"` @@ -246,15 +371,26 @@ type OperatorLogicalBackupConfiguration struct { S3SSE string `json:"logical_backup_s3_sse,omitempty"` RetentionTime string `json:"logical_backup_s3_retention_time,omitempty"` GoogleApplicationCredentials string `json:"logical_backup_google_application_credentials,omitempty"` - JobPrefix string `json:"logical_backup_job_prefix,omitempty"` - CronjobEnvironmentSecret string `json:"logical_backup_cronjob_environment_secret,omitempty"` - CPURequest string `json:"logical_backup_cpu_request,omitempty"` - MemoryRequest string `json:"logical_backup_memory_request,omitempty"` - CPULimit string `json:"logical_backup_cpu_limit,omitempty"` - MemoryLimit string `json:"logical_backup_memory_limit,omitempty"` - SuccessfulJobsHistoryLimit *int32 `json:"logical_backup_successful_jobs_history_limit,omitempty"` - FailedJobsHistoryLimit *int32 `json:"logical_backup_failed_jobs_history_limit,omitempty"` - TTLSecondsAfterFinished *int32 `json:"logical_backup_ttl_seconds_after_finished,omitempty"` + // +kubebuilder:default=logical-backup- + JobPrefix string `json:"logical_backup_job_prefix,omitempty"` + CronjobEnvironmentSecret string `json:"logical_backup_cronjob_environment_secret,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + CPURequest string `json:"logical_backup_cpu_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + MemoryRequest string `json:"logical_backup_memory_request,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+m|\d+(\.\d{1,3})?)$` + CPULimit string `json:"logical_backup_cpu_limit,omitempty"` + // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` + MemoryLimit string `json:"logical_backup_memory_limit,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=3 + SuccessfulJobsHistoryLimit *int32 `json:"logical_backup_successful_jobs_history_limit,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=3 + FailedJobsHistoryLimit *int32 `json:"logical_backup_failed_jobs_history_limit,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=86400 + TTLSecondsAfterFinished *int32 `json:"logical_backup_ttl_seconds_after_finished,omitempty"` } // PatroniConfiguration defines configuration for Patroni @@ -264,42 +400,80 @@ type PatroniConfiguration struct { // OperatorConfigurationData defines the operation config type OperatorConfigurationData struct { - EnableCRDRegistration *bool `json:"enable_crd_registration,omitempty"` - EnableCRDValidation *bool `json:"enable_crd_validation,omitempty"` - CRDCategories []string `json:"crd_categories,omitempty"` - EnableLazySpiloUpgrade bool `json:"enable_lazy_spilo_upgrade,omitempty"` - EnablePgVersionEnvVar bool `json:"enable_pgversion_env_var,omitempty"` - EnableSpiloWalPathCompat bool `json:"enable_spilo_wal_path_compat,omitempty"` - EnableTeamIdClusternamePrefix bool `json:"enable_team_id_clustername_prefix,omitempty"` - EtcdHost string `json:"etcd_host,omitempty"` - KubernetesUseConfigMaps bool `json:"kubernetes_use_configmaps,omitempty"` - DockerImage string `json:"docker_image,omitempty"` - Workers uint32 `json:"workers,omitempty"` - ResyncPeriod Duration `json:"resync_period,omitempty"` - RepairPeriod Duration `json:"repair_period,omitempty"` - EnableMaintenanceWindows *bool `json:"enable_maintenance_windows,omitempty"` - MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"` - SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"` - ShmVolume *bool `json:"enable_shm_volume,omitempty"` - SidecarImages map[string]string `json:"sidecar_docker_images,omitempty"` // deprecated in favour of SidecarContainers - SidecarContainers []v1.Container `json:"sidecars,omitempty"` - PostgresUsersConfiguration PostgresUsersConfiguration `json:"users"` - MajorVersionUpgrade MajorVersionUpgradeConfiguration `json:"major_version_upgrade"` - Kubernetes KubernetesMetaConfiguration `json:"kubernetes"` - PostgresPodResources PostgresPodResourcesDefaults `json:"postgres_pod_resources"` - Timeouts OperatorTimeouts `json:"timeouts"` - LoadBalancer LoadBalancerConfiguration `json:"load_balancer"` - AWSGCP AWSGCPConfiguration `json:"aws_or_gcp"` - OperatorDebug OperatorDebugConfiguration `json:"debug"` - TeamsAPI TeamsAPIConfiguration `json:"teams_api"` - LoggingRESTAPI LoggingRESTAPIConfiguration `json:"logging_rest_api"` - Scalyr ScalyrConfiguration `json:"scalyr"` - LogicalBackup OperatorLogicalBackupConfiguration `json:"logical_backup"` - ConnectionPooler ConnectionPoolerConfiguration `json:"connection_pooler"` - Patroni PatroniConfiguration `json:"patroni"` - - MinInstances int32 `json:"min_instances,omitempty"` - MaxInstances int32 `json:"max_instances,omitempty"` + // +kubebuilder:default=true + EnableCRDRegistration *bool `json:"enable_crd_registration,omitempty"` + CRDCategories []string `json:"crd_categories,omitempty"` + EnableLazySpiloUpgrade bool `json:"enable_lazy_spilo_upgrade,omitempty"` + // +kubebuilder:default=true + EnablePgVersionEnvVar bool `json:"enable_pgversion_env_var,omitempty"` + EnableSpiloWalPathCompat bool `json:"enable_spilo_wal_path_compat,omitempty"` + EnableTeamIdClusternamePrefix bool `json:"enable_team_id_clustername_prefix,omitempty"` + // +kubebuilder:default="" + EtcdHost string `json:"etcd_host,omitempty"` + // +kubebuilder:default=true + KubernetesUseConfigMaps bool `json:"kubernetes_use_configmaps,omitempty"` + // +kubebuilder:default="ghcr.io/zalando/spilo-18:4.1-p1" + DockerImage string `json:"docker_image,omitempty"` + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:default=8 + Workers uint32 `json:"workers,omitempty"` + // +kubebuilder:default="30m" + // period between consecutive sync requests + ResyncPeriod Duration `json:"resync_period,omitempty"` + // +kubebuilder:default="5m" + // period between consecutive repair requests + RepairPeriod Duration `json:"repair_period,omitempty"` + // +kubebuilder:default=true + EnableMaintenanceWindows *bool `json:"enable_maintenance_windows,omitempty"` + // +kubebuilder:validation:Schemaless + // +kubebuilder:validation:Type=array + MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"` + SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"` + // +kubebuilder:default=true + ShmVolume *bool `json:"enable_shm_volume,omitempty"` + SidecarImages map[string]string `json:"sidecar_docker_images,omitempty"` // deprecated in favour of SidecarContainers + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + SidecarContainers []v1.Container `json:"sidecars,omitempty"` + // +optional + PostgresUsersConfiguration PostgresUsersConfiguration `json:"users"` + // +optional + MajorVersionUpgrade MajorVersionUpgradeConfiguration `json:"major_version_upgrade"` + // +optional + Kubernetes KubernetesMetaConfiguration `json:"kubernetes"` + // +optional + PostgresPodResources PostgresPodResourcesDefaults `json:"postgres_pod_resources"` + // +optional + Timeouts OperatorTimeouts `json:"timeouts"` + // +optional + LoadBalancer LoadBalancerConfiguration `json:"load_balancer"` + // +optional + AWSGCP AWSGCPConfiguration `json:"aws_or_gcp"` + // +optional + OperatorDebug OperatorDebugConfiguration `json:"debug"` + // +optional + TeamsAPI TeamsAPIConfiguration `json:"teams_api"` + // +optional + LoggingRESTAPI LoggingRESTAPIConfiguration `json:"logging_rest_api"` + // +optional + Scalyr ScalyrConfiguration `json:"scalyr"` + // +optional + LogicalBackup OperatorLogicalBackupConfiguration `json:"logical_backup"` + // +optional + ConnectionPooler ConnectionPoolerConfiguration `json:"connection_pooler"` + // +optional + Patroni PatroniConfiguration `json:"patroni"` + + // +kubebuilder:validation:Minimum=-1 + // +kubebuilder:default=-1 + // -1 = disabled + MinInstances int32 `json:"min_instances,omitempty"` + // +kubebuilder:validation:Minimum=-1 + // +kubebuilder:default=-1 + // -1 = disabled + MaxInstances int32 `json:"max_instances,omitempty"` + IgnoreInstanceLimitsAnnotationKey string `json:"ignore_instance_limits_annotation_key,omitempty"` IgnoreResourcesLimitsAnnotationKey string `json:"ignore_resources_limits_annotation_key,omitempty"` } diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml new file mode 100644 index 000000000..5f347f2ac --- /dev/null +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -0,0 +1,971 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: operatorconfigurations.acid.zalan.do +spec: + group: acid.zalan.do + names: + categories: + - all + kind: OperatorConfiguration + listKind: OperatorConfigurationList + plural: operatorconfigurations + shortNames: + - opconfig + singular: operatorconfiguration + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Spilo image to be used for Pods + jsonPath: .configuration.docker_image + name: Image + type: string + - description: Label for K8s resources created by operator + jsonPath: .configuration.kubernetes.cluster_name_label + name: Cluster-Label + type: string + - description: Name of service account to be used + jsonPath: .configuration.kubernetes.pod_service_account_name + name: Service-Account + type: string + - description: Minimum number of instances per Postgres cluster + jsonPath: .configuration.min_instances + name: Min-Instances + type: integer + - description: Age of the OperatorConfiguration resource + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: OperatorConfiguration defines the specification for the OperatorConfiguration. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + configuration: + description: OperatorConfigurationData defines the operation config + properties: + aws_or_gcp: + description: AWSGCPConfiguration defines the configuration for AWS + properties: + additional_secret_mount: + type: string + additional_secret_mount_path: + type: string + aws_region: + default: eu-central-1 + type: string + enable_ebs_gp3_migration: + type: boolean + enable_ebs_gp3_migration_max_size: + format: int64 + type: integer + gcp_credentials: + type: string + kube_iam_role: + type: string + log_s3_bucket: + type: string + wal_az_storage_account: + type: string + wal_gs_bucket: + type: string + wal_s3_bucket: + type: string + type: object + connection_pooler: + description: ConnectionPoolerConfiguration defines default configuration + for connection pooler + properties: + connection_pooler_default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_image: + default: ghcr.io/zalando/postgres-operator/pgbouncer:latest + type: string + connection_pooler_max_db_connections: + format: int32 + type: integer + connection_pooler_mode: + default: transaction + enum: + - session + - transaction + type: string + connection_pooler_number_of_instances: + default: 2 + format: int32 + minimum: 1 + type: integer + connection_pooler_schema: + default: pooler + type: string + connection_pooler_user: + default: pooler + type: string + type: object + crd_categories: + items: + type: string + type: array + debug: + description: OperatorDebugConfiguration defines options for the debug + mode + properties: + debug_logging: + default: true + type: boolean + enable_database_access: + default: true + type: boolean + type: object + docker_image: + default: ghcr.io/zalando/spilo-18:4.1-p1 + type: string + enable_crd_registration: + default: true + type: boolean + enable_lazy_spilo_upgrade: + type: boolean + enable_maintenance_windows: + default: true + type: boolean + enable_pgversion_env_var: + default: true + type: boolean + enable_shm_volume: + default: true + type: boolean + enable_spilo_wal_path_compat: + type: boolean + enable_team_id_clustername_prefix: + type: boolean + etcd_host: + default: "" + type: string + ignore_instance_limits_annotation_key: + type: string + ignore_resources_limits_annotation_key: + type: string + kubernetes: + description: KubernetesMetaConfiguration defines k8s conf required + for all Postgres clusters and the operator itself + properties: + additional_pod_capabilities: + items: + type: string + type: array + cluster_domain: + default: cluster.local + type: string + cluster_labels: + additionalProperties: + type: string + default: + application: spilo + type: object + cluster_name_label: + default: cluster-name + type: string + custom_pod_annotations: + additionalProperties: + type: string + type: object + delete_annotation_date_key: + type: string + delete_annotation_name_key: + type: string + downscaler_annotations: + items: + type: string + type: array + enable_cross_namespace_secret: + type: boolean + enable_finalizers: + type: boolean + enable_init_containers: + default: true + type: boolean + enable_owner_references: + type: boolean + enable_persistent_volume_claim_deletion: + default: true + type: boolean + enable_pod_antiaffinity: + type: boolean + enable_pod_disruption_budget: + default: true + type: boolean + enable_readiness_probe: + type: boolean + enable_secrets_deletion: + default: true + type: boolean + enable_sidecars: + default: true + type: boolean + ignored_annotations: + items: + type: string + type: array + infrastructure_roles_secret_name: + description: |- + NamespacedName comprises a resource name, with a mandatory namespace, + rendered as "/". Being a type captures intent and + helps make sure that UIDs, namespaced names and non-namespaced names + do not get conflated in code. For most use cases, namespace and name + will already have been format validated at the API entry point, so we + don't do that here. Where that's not the case (e.g. in testing), + consider using NamespacedNameOrDie() in testing.go in this package. + + from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + infrastructure_roles_secrets: + description: namespaced name of the secret containing infrastructure + roles names and passwords + items: + properties: + defaultrolevalue: + type: string + defaultuservalue: + type: string + details: + description: This field point out the detailed yaml definition + of the role, if exists + type: string + passwordkey: + type: string + rolekey: + type: string + secretname: + description: |- + Name of a secret which describes the role, and optionally name of a + configmap with an extra information + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + template: + type: boolean + userkey: + type: string + type: object + type: array + inherited_annotations: + items: + type: string + type: array + inherited_labels: + items: + type: string + type: array + liveness_probe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + master_pod_move_timeout: + default: 20m + description: timeout for successful migration of master pods from + unschedulable node + format: int64 + type: integer + node_readiness_label: + additionalProperties: + type: string + type: object + node_readiness_label_merge: + enum: + - AND + - OR + type: string + oauth_token_secret_name: + default: postgres-operator + description: namespaced name of the secret containing the OAuth2 + token to pass to the teams API + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + pdb_master_label_selector: + default: true + type: boolean + pdb_name_format: + default: postgres-{cluster}-pdb + description: defines the template for PDB names + type: string + persistent_volume_claim_retention_policy: + additionalProperties: + type: string + type: object + pod_antiaffinity_preferred_during_scheduling: + type: boolean + pod_antiaffinity_topology_key: + default: kubernetes.io/hostname + type: string + pod_environment_configmap: + description: namespaced name of the ConfigMap with environment + variables to populate on every pod + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + pod_environment_secret: + type: string + pod_management_policy: + default: ordered_ready + enum: + - ordered_ready + - parallel + type: string + pod_priority_class_name: + type: string + pod_role_label: + default: spilo-role + type: string + pod_service_account_definition: + type: string + pod_service_account_name: + default: postgres-pod + type: string + pod_service_account_role_binding_definition: + type: string + pod_terminate_grace_period: + default: 5m + description: Postgres pods are terminated forcefully after this + timeout + format: int64 + type: integer + secret_name_template: + default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' + description: |- + template for database user secrets generated by the operator, + here username contains the namespace in the format namespace.username + if the user is in different namespace than cluster and cross namespace secrets + are enabled via `enable_cross_namespace_secret` flag in the configuration. + type: string + share_pgsocket_with_sidecars: + type: boolean + spilo_allow_privilege_escalation: + default: true + type: boolean + spilo_fsgroup: + format: int64 + type: integer + spilo_privileged: + type: boolean + spilo_runasgroup: + format: int64 + type: integer + spilo_runasuser: + format: int64 + type: integer + storage_resize_mode: + default: pvc + enum: + - ebs + - mixed + - pvc + - "off" + type: string + toleration: + additionalProperties: + type: string + type: object + watched_namespace: + type: string + type: object + kubernetes_use_configmaps: + default: true + type: boolean + load_balancer: + description: LoadBalancerConfiguration defines the LB configuration + properties: + custom_service_annotations: + additionalProperties: + type: string + type: object + db_hosted_zone: + type: string + enable_master_load_balancer: + type: boolean + enable_master_node_port: + type: boolean + enable_master_pooler_load_balancer: + type: boolean + enable_master_pooler_node_port: + type: boolean + enable_replica_load_balancer: + type: boolean + enable_replica_node_port: + type: boolean + enable_replica_pooler_load_balancer: + type: boolean + enable_replica_pooler_node_port: + type: boolean + external_traffic_policy: + default: Cluster + enum: + - Cluster + - Local + type: string + master_dns_name_format: + default: '{cluster}.{namespace}.{hostedzone}' + description: defines the DNS name string template for the master + load balancer cluster + type: string + master_legacy_dns_name_format: + default: '{cluster}.{team}.{hostedzone}' + description: deprecated DNS template for master load balancer + using team name + type: string + replica_dns_name_format: + default: '{cluster}-repl.{namespace}.{hostedzone}' + description: defines the DNS name string template for the replica + load balancer cluster + type: string + replica_legacy_dns_name_format: + default: '{cluster}-repl.{team}.{hostedzone}' + description: deprecated DNS template for replica load balancer + using team name + type: string + type: object + logging_rest_api: + description: LoggingRESTAPIConfiguration defines Logging API conf + properties: + api_port: + default: 8080 + type: integer + cluster_history_entries: + default: 1000 + type: integer + ring_log_lines: + default: 100 + type: integer + type: object + logical_backup: + description: OperatorLogicalBackupConfiguration defines configuration + for logical backup + properties: + logical_backup_azure_storage_account_key: + type: string + logical_backup_azure_storage_account_name: + type: string + logical_backup_azure_storage_container: + type: string + logical_backup_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + logical_backup_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + logical_backup_cronjob_environment_secret: + type: string + logical_backup_docker_image: + default: ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1 + type: string + logical_backup_failed_jobs_history_limit: + default: 3 + format: int32 + minimum: 0 + type: integer + logical_backup_google_application_credentials: + type: string + logical_backup_job_prefix: + default: logical-backup- + type: string + logical_backup_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + logical_backup_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + logical_backup_provider: + default: s3 + enum: + - az + - gcs + - s3 + type: string + logical_backup_s3_access_key_id: + type: string + logical_backup_s3_bucket: + type: string + logical_backup_s3_bucket_prefix: + type: string + logical_backup_s3_endpoint: + type: string + logical_backup_s3_region: + type: string + logical_backup_s3_retention_time: + type: string + logical_backup_s3_secret_access_key: + type: string + logical_backup_s3_sse: + type: string + logical_backup_schedule: + default: 30 00 * * * + pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$ + type: string + logical_backup_successful_jobs_history_limit: + default: 3 + format: int32 + minimum: 0 + type: integer + logical_backup_ttl_seconds_after_finished: + default: 86400 + format: int32 + minimum: 0 + type: integer + type: object + maintenance_windows: + type: array + major_version_upgrade: + description: MajorVersionUpgradeConfiguration defines how to execute + major version upgrades of Postgres. + properties: + major_version_upgrade_mode: + default: manual + enum: + - "off" + - manual + - full + type: string + major_version_upgrade_team_allow_list: + items: + type: string + type: array + minimal_major_version: + default: "14" + type: string + target_major_version: + default: "18" + type: string + type: object + max_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + min_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + patroni: + description: PatroniConfiguration defines configuration for Patroni + properties: + enable_patroni_failsafe_mode: + type: boolean + type: object + postgres_pod_resources: + description: PostgresPodResourcesDefaults defines the spec of default + resources + properties: + default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + max_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + max_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + min_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + min_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + type: object + repair_period: + default: 5m + description: period between consecutive repair requests + format: int64 + type: integer + resync_period: + default: 30m + description: period between consecutive sync requests + format: int64 + type: integer + scalyr: + description: ScalyrConfiguration defines the configuration for ScalyrAPI + properties: + scalyr_api_key: + type: string + scalyr_cpu_limit: + default: "1" + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_cpu_request: + default: 100m + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_image: + type: string + scalyr_memory_limit: + default: 500Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_memory_request: + default: 50Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_server_url: + default: https://upload.eu.scalyr.com + type: string + type: object + set_memory_request_to_limit: + type: boolean + sidecar_docker_images: + additionalProperties: + type: string + type: object + sidecars: + type: object + x-kubernetes-preserve-unknown-fields: true + teams_api: + description: TeamsAPIConfiguration defines the configuration of TeamsAPI + properties: + enable_admin_role_for_users: + default: true + type: boolean + enable_postgres_team_crd: + default: true + type: boolean + enable_postgres_team_crd_superusers: + type: boolean + enable_team_member_deprecation: + type: boolean + enable_team_superuser: + type: boolean + enable_teams_api: + type: boolean + pam_configuration: + default: https://info.example.com/oauth2/tokeninfo?access_token= + uid realm=/employees + type: string + pam_role_name: + default: zalandos + type: string + postgres_superuser_teams: + items: + type: string + type: array + protected_role_names: + default: '["admin", "cron_admin"]' + items: + type: string + type: array + role_deletion_suffix: + default: _deleted + type: string + team_admin_role: + default: admin + type: string + team_api_role_configuration: + additionalProperties: + type: string + default: + log_statement: all + type: object + teams_api_url: + default: https://teams.example.com/api/ + type: string + type: object + timeouts: + description: OperatorTimeouts defines the timeout of ResourceCheck, + PodWait, ReadyWait + properties: + patroni_api_check_interval: + default: 1s + description: interval between consecutive attempts of operator + calling the Patroni API + format: int64 + type: integer + patroni_api_check_timeout: + default: 5s + description: timeout when waiting for successful response from + Patroni API + format: int64 + type: integer + pod_deletion_wait_timeout: + default: 10m + description: timeout when waiting for the Postgres pods to be + deleted + format: int64 + type: integer + pod_label_wait_timeout: + default: 10m + description: timeout when waiting for pod role and cluster labels + format: int64 + type: integer + ready_wait_interval: + default: 4s + description: interval between consecutive attempts waiting for + postgresql CRD to be created + format: int64 + type: integer + ready_wait_timeout: + default: 30s + description: timeout for the complete postgres CRD creation + format: int64 + type: integer + resource_check_interval: + default: 3s + description: interval to wait between consecutive attempts to + check for some K8s resources + format: int64 + type: integer + resource_check_timeout: + default: 10m + description: timeout when waiting for the presence of a certain + K8s resource + format: int64 + type: integer + type: object + users: + description: PostgresUsersConfiguration defines the system users of + Postgres. + properties: + additional_owner_roles: + items: + type: string + type: array + enable_password_rotation: + type: boolean + password_rotation_interval: + default: 90 + format: int32 + type: integer + password_rotation_user_retention: + default: 120 + format: int32 + type: integer + replication_username: + default: standby + type: string + super_username: + default: postgres + type: string + type: object + workers: + default: 8 + format: int32 + minimum: 1 + type: integer + type: object + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + required: + - configuration + - metadata + type: object + served: true + storage: true + subresources: + status: {} 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 9005b0bbe..7d18c2cf2 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -428,11 +428,6 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData *out = new(bool) **out = **in } - if in.EnableCRDValidation != nil { - in, out := &in.EnableCRDValidation, &out.EnableCRDValidation - *out = new(bool) - **out = **in - } if in.CRDCategories != nil { in, out := &in.CRDCategories, &out.CRDCategories *out = make([]string, len(*in)) diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 4514e7487..66fc7a731 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -31,7 +31,6 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur // general config result.EnableCRDRegistration = util.CoalesceBool(fromCRD.EnableCRDRegistration, util.True()) - result.EnableCRDValidation = util.CoalesceBool(fromCRD.EnableCRDValidation, util.True()) result.CRDCategories = util.CoalesceStrArr(fromCRD.CRDCategories, []string{"all"}) result.EnableLazySpiloUpgrade = fromCRD.EnableLazySpiloUpgrade result.EnablePgVersionEnvVar = fromCRD.EnablePgVersionEnvVar diff --git a/pkg/controller/util.go b/pkg/controller/util.go index 87962f7b9..f58c9df5a 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -103,7 +103,11 @@ func (c *Controller) createPostgresCRD() error { } func (c *Controller) createConfigurationCRD() error { - return c.createOperatorCRD(acidv1.ConfigurationCRD(c.opConfig.CRDCategories)) + crd, err := acidv1.OperatorConfigurationCRD(c.opConfig.CRDCategories) + if err != nil { + return fmt.Errorf("could not create OperatorConfiguration CRD object: %v", err) + } + return c.createOperatorCRD(crd) } func readDecodedRole(s string) (*spec.PgUser, error) { diff --git a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_operatorconfiguration.go b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_operatorconfiguration.go index 8c9790d18..66db8ec40 100644 --- a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_operatorconfiguration.go +++ b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_operatorconfiguration.go @@ -32,18 +32,26 @@ import ( // fakeOperatorConfigurations implements OperatorConfigurationInterface type fakeOperatorConfigurations struct { - *gentype.FakeClient[*v1.OperatorConfiguration] + *gentype.FakeClientWithList[*v1.OperatorConfiguration, *v1.OperatorConfigurationList] Fake *FakeAcidV1 } func newFakeOperatorConfigurations(fake *FakeAcidV1, namespace string) acidzalandov1.OperatorConfigurationInterface { return &fakeOperatorConfigurations{ - gentype.NewFakeClient[*v1.OperatorConfiguration]( + gentype.NewFakeClientWithList[*v1.OperatorConfiguration, *v1.OperatorConfigurationList]( fake.Fake, namespace, v1.SchemeGroupVersion.WithResource("operatorconfigurations"), v1.SchemeGroupVersion.WithKind("OperatorConfiguration"), func() *v1.OperatorConfiguration { return &v1.OperatorConfiguration{} }, + func() *v1.OperatorConfigurationList { return &v1.OperatorConfigurationList{} }, + func(dst, src *v1.OperatorConfigurationList) { dst.ListMeta = src.ListMeta }, + func(list *v1.OperatorConfigurationList) []*v1.OperatorConfiguration { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1.OperatorConfigurationList, items []*v1.OperatorConfiguration) { + list.Items = gentype.FromPointerSlice(items) + }, ), fake, } diff --git a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/operatorconfiguration.go b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/operatorconfiguration.go index 91dc27037..292fa2fce 100644 --- a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/operatorconfiguration.go +++ b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/operatorconfiguration.go @@ -30,6 +30,8 @@ import ( acidzalandov1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" scheme "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" ) @@ -41,24 +43,32 @@ type OperatorConfigurationsGetter interface { // OperatorConfigurationInterface has methods to work with OperatorConfiguration resources. type OperatorConfigurationInterface interface { + Create(ctx context.Context, operatorConfiguration *acidzalandov1.OperatorConfiguration, opts metav1.CreateOptions) (*acidzalandov1.OperatorConfiguration, error) + Update(ctx context.Context, operatorConfiguration *acidzalandov1.OperatorConfiguration, opts metav1.UpdateOptions) (*acidzalandov1.OperatorConfiguration, error) + Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error Get(ctx context.Context, name string, opts metav1.GetOptions) (*acidzalandov1.OperatorConfiguration, error) + List(ctx context.Context, opts metav1.ListOptions) (*acidzalandov1.OperatorConfigurationList, error) + Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *acidzalandov1.OperatorConfiguration, err error) OperatorConfigurationExpansion } // operatorConfigurations implements OperatorConfigurationInterface type operatorConfigurations struct { - *gentype.Client[*acidzalandov1.OperatorConfiguration] + *gentype.ClientWithList[*acidzalandov1.OperatorConfiguration, *acidzalandov1.OperatorConfigurationList] } // newOperatorConfigurations returns a OperatorConfigurations func newOperatorConfigurations(c *AcidV1Client, namespace string) *operatorConfigurations { return &operatorConfigurations{ - gentype.NewClient[*acidzalandov1.OperatorConfiguration]( + gentype.NewClientWithList[*acidzalandov1.OperatorConfiguration, *acidzalandov1.OperatorConfigurationList]( "operatorconfigurations", c.RESTClient(), scheme.ParameterCodec, namespace, func() *acidzalandov1.OperatorConfiguration { return &acidzalandov1.OperatorConfiguration{} }, + func() *acidzalandov1.OperatorConfigurationList { return &acidzalandov1.OperatorConfigurationList{} }, ), } } diff --git a/pkg/generated/informers/externalversions/acid.zalan.do/v1/interface.go b/pkg/generated/informers/externalversions/acid.zalan.do/v1/interface.go index 1ea652a3f..d176a2b35 100644 --- a/pkg/generated/informers/externalversions/acid.zalan.do/v1/interface.go +++ b/pkg/generated/informers/externalversions/acid.zalan.do/v1/interface.go @@ -30,6 +30,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // OperatorConfigurations returns a OperatorConfigurationInformer. + OperatorConfigurations() OperatorConfigurationInformer // PostgresTeams returns a PostgresTeamInformer. PostgresTeams() PostgresTeamInformer // Postgresqls returns a PostgresqlInformer. @@ -47,6 +49,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// OperatorConfigurations returns a OperatorConfigurationInformer. +func (v *version) OperatorConfigurations() OperatorConfigurationInformer { + return &operatorConfigurationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // PostgresTeams returns a PostgresTeamInformer. func (v *version) PostgresTeams() PostgresTeamInformer { return &postgresTeamInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go b/pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go new file mode 100644 index 000000000..1522adc49 --- /dev/null +++ b/pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go @@ -0,0 +1,96 @@ +/* +Copyright 2026 Compose, Zalando SE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1 + +import ( + context "context" + time "time" + + apisacidzalandov1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + versioned "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned" + internalinterfaces "github.com/zalando/postgres-operator/pkg/generated/informers/externalversions/internalinterfaces" + acidzalandov1 "github.com/zalando/postgres-operator/pkg/generated/listers/acid.zalan.do/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// OperatorConfigurationInformer provides access to a shared informer and lister for +// OperatorConfigurations. +type OperatorConfigurationInformer interface { + Informer() cache.SharedIndexInformer + Lister() acidzalandov1.OperatorConfigurationLister +} + +type operatorConfigurationInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewOperatorConfigurationInformer constructs a new informer for OperatorConfiguration type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewOperatorConfigurationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredOperatorConfigurationInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredOperatorConfigurationInformer constructs a new informer for OperatorConfiguration type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredOperatorConfigurationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AcidV1().OperatorConfigurations(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AcidV1().OperatorConfigurations(namespace).Watch(context.TODO(), options) + }, + }, + &apisacidzalandov1.OperatorConfiguration{}, + resyncPeriod, + indexers, + ) +} + +func (f *operatorConfigurationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredOperatorConfigurationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *operatorConfigurationInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisacidzalandov1.OperatorConfiguration{}, f.defaultInformer) +} + +func (f *operatorConfigurationInformer) Lister() acidzalandov1.OperatorConfigurationLister { + return acidzalandov1.NewOperatorConfigurationLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index ed27d5743..f5953bde6 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -60,6 +60,8 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=acid.zalan.do, Version=v1 + case v1.SchemeGroupVersion.WithResource("operatorconfigurations"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Acid().V1().OperatorConfigurations().Informer()}, nil case v1.SchemeGroupVersion.WithResource("postgresteams"): return &genericInformer{resource: resource.GroupResource(), informer: f.Acid().V1().PostgresTeams().Informer()}, nil case v1.SchemeGroupVersion.WithResource("postgresqls"): diff --git a/pkg/generated/listers/acid.zalan.do/v1/expansion_generated.go b/pkg/generated/listers/acid.zalan.do/v1/expansion_generated.go index b71f44767..f9b4c79e7 100644 --- a/pkg/generated/listers/acid.zalan.do/v1/expansion_generated.go +++ b/pkg/generated/listers/acid.zalan.do/v1/expansion_generated.go @@ -24,6 +24,14 @@ SOFTWARE. package v1 +// OperatorConfigurationListerExpansion allows custom methods to be added to +// OperatorConfigurationLister. +type OperatorConfigurationListerExpansion interface{} + +// OperatorConfigurationNamespaceListerExpansion allows custom methods to be added to +// OperatorConfigurationNamespaceLister. +type OperatorConfigurationNamespaceListerExpansion interface{} + // PostgresTeamListerExpansion allows custom methods to be added to // PostgresTeamLister. type PostgresTeamListerExpansion interface{} diff --git a/pkg/generated/listers/acid.zalan.do/v1/operatorconfiguration.go b/pkg/generated/listers/acid.zalan.do/v1/operatorconfiguration.go new file mode 100644 index 000000000..c00599718 --- /dev/null +++ b/pkg/generated/listers/acid.zalan.do/v1/operatorconfiguration.go @@ -0,0 +1,76 @@ +/* +Copyright 2026 Compose, Zalando SE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1 + +import ( + acidzalandov1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// OperatorConfigurationLister helps list OperatorConfigurations. +// All objects returned here must be treated as read-only. +type OperatorConfigurationLister interface { + // List lists all OperatorConfigurations in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*acidzalandov1.OperatorConfiguration, err error) + // OperatorConfigurations returns an object that can list and get OperatorConfigurations. + OperatorConfigurations(namespace string) OperatorConfigurationNamespaceLister + OperatorConfigurationListerExpansion +} + +// operatorConfigurationLister implements the OperatorConfigurationLister interface. +type operatorConfigurationLister struct { + listers.ResourceIndexer[*acidzalandov1.OperatorConfiguration] +} + +// NewOperatorConfigurationLister returns a new OperatorConfigurationLister. +func NewOperatorConfigurationLister(indexer cache.Indexer) OperatorConfigurationLister { + return &operatorConfigurationLister{listers.New[*acidzalandov1.OperatorConfiguration](indexer, acidzalandov1.Resource("operatorconfiguration"))} +} + +// OperatorConfigurations returns an object that can list and get OperatorConfigurations. +func (s *operatorConfigurationLister) OperatorConfigurations(namespace string) OperatorConfigurationNamespaceLister { + return operatorConfigurationNamespaceLister{listers.NewNamespaced[*acidzalandov1.OperatorConfiguration](s.ResourceIndexer, namespace)} +} + +// OperatorConfigurationNamespaceLister helps list and get OperatorConfigurations. +// All objects returned here must be treated as read-only. +type OperatorConfigurationNamespaceLister interface { + // List lists all OperatorConfigurations in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*acidzalandov1.OperatorConfiguration, err error) + // Get retrieves the OperatorConfiguration from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*acidzalandov1.OperatorConfiguration, error) + OperatorConfigurationNamespaceListerExpansion +} + +// operatorConfigurationNamespaceLister implements the OperatorConfigurationNamespaceLister +// interface. +type operatorConfigurationNamespaceLister struct { + listers.ResourceIndexer[*acidzalandov1.OperatorConfiguration] +} diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 06edac439..43fa37a33 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -19,7 +19,6 @@ type CRD struct { ResyncPeriod time.Duration `name:"resync_period" default:"30m"` RepairPeriod time.Duration `name:"repair_period" default:"5m"` EnableCRDRegistration *bool `name:"enable_crd_registration" default:"true"` - EnableCRDValidation *bool `name:"enable_crd_validation" default:"true"` CRDCategories []string `name:"crd_categories" default:"all"` } From df3224730f7603b9c8ecb9cc7282622136b4f223 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Tue, 23 Jun 2026 13:35:53 +0200 Subject: [PATCH 34/70] Update to Go 1.26.4, build runners and go.mod depedencies (#3108) * update golang and dependencies * fix incorrect log formatting * clean mod chache and introduce GOARCH in Dockerfile (choose dynamically) * remove GO111MODULE mentions * bump github actions from v2 to v3 * bump docker runners to v7 * use extra event store for backwards compatibility with existing codebase * updated generated opconfig api --- .github/workflows/publish_ghcr_image.yaml | 20 +- .github/workflows/run_e2e.yaml | 6 +- .github/workflows/run_tests.yaml | 6 +- Makefile | 11 +- README.md | 1 + docker/DebugDockerfile | 2 +- docker/Dockerfile | 13 +- docker/build_operator.sh | 5 +- e2e/Dockerfile | 2 +- e2e/Makefile | 2 +- go.mod | 98 ++--- go.sum | 219 ++++++------ hack/update-codegen.sh | 1 - manifests/postgresql.crd.yaml | 338 ++++++++++++++++-- pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 338 ++++++++++++++++-- pkg/cluster/cluster.go | 28 +- pkg/cluster/k8sres.go | 6 +- pkg/cluster/majorversionupgrade.go | 2 +- pkg/cluster/volumes.go | 8 +- pkg/cluster/volumes_test.go | 2 +- pkg/controller/controller.go | 11 +- pkg/controller/logs_and_api.go | 10 +- pkg/controller/postgresql.go | 40 ++- pkg/controller/util.go | 2 +- .../versioned/fake/clientset_generated.go | 22 +- .../acid.zalan.do/v1/acid.zalan.do_client.go | 12 +- .../zalando.org/v1/zalando.org_client.go | 12 +- .../acid.zalan.do/v1/operatorconfiguration.go | 52 ++- .../acid.zalan.do/v1/postgresql.go | 52 ++- .../acid.zalan.do/v1/postgresteam.go | 52 ++- .../informers/externalversions/factory.go | 113 ++++-- .../internalinterfaces/factory_interfaces.go | 19 + .../zalando.org/v1/fabriceventstream.go | 52 ++- pkg/util/volumes/ebs.go | 67 ++-- 34 files changed, 1219 insertions(+), 405 deletions(-) diff --git a/.github/workflows/publish_ghcr_image.yaml b/.github/workflows/publish_ghcr_image.yaml index 5a0c3b045..2fc22d0c9 100644 --- a/.github/workflows/publish_ghcr_image.yaml +++ b/.github/workflows/publish_ghcr_image.yaml @@ -19,11 +19,11 @@ jobs: packages: write steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v6 with: - go-version: "^1.25.3" + go-version: "^1.26.4" - name: Run unit tests run: make test @@ -53,20 +53,20 @@ jobs: echo "BACKUP_IMAGE=$BACKUP_IMAGE" >> $GITHUB_OUTPUT - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v4 - name: Login to GHCR - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push multiarch operator image to ghcr - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: context: . file: docker/Dockerfile @@ -76,7 +76,7 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Build and push multiarch pooler image to ghcr - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: context: pooler push: true @@ -85,7 +85,7 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Build and push multiarch ui image to ghcr - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: context: ui push: true @@ -94,7 +94,7 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Build and push multiarch logical-backup image to ghcr - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: context: logical-backup push: true diff --git a/.github/workflows/run_e2e.yaml b/.github/workflows/run_e2e.yaml index e7c04c0c5..393109b00 100644 --- a/.github/workflows/run_e2e.yaml +++ b/.github/workflows/run_e2e.yaml @@ -11,10 +11,10 @@ jobs: name: End-2-End tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: - go-version: "^1.25.3" + go-version: "^1.26.4" - name: Make dependencies run: make mocks - name: Code generation diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 7940b61f2..c511e8fa1 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -11,10 +11,10 @@ jobs: name: Unit tests and coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: - go-version: "^1.25.3" + go-version: "^1.26.4" - name: Make dependencies run: make mocks - name: Compile diff --git a/Makefile b/Makefile index 323b51892..3613c1044 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ BINARY ?= postgres-operator BUILD_FLAGS ?= -v +GOARCH ?= amd64 CGO_ENABLED ?= 0 ifeq ($(RACE),1) BUILD_FLAGS += -race -a @@ -83,10 +84,10 @@ wasm: ${SOURCES} $(GENERATED_CRDS) GOOS=wasip1 GOARCH=wasm CGO_ENABLED=${CGO_ENABLED} go build -o build/${BINARY}.wasm ${BUILD_FLAGS} -ldflags "$(LDFLAGS)" $(SOURCES) linux: ${SOURCES} $(GENERATED_CRDS) - GOOS=linux GOARCH=amd64 CGO_ENABLED=${CGO_ENABLED} go build -o build/linux/${BINARY} ${BUILD_FLAGS} -ldflags "$(LDFLAGS)" $(SOURCES) + GOOS=linux GOARCH=${GOARCH} CGO_ENABLED=${CGO_ENABLED} go build -o build/linux/${BINARY} ${BUILD_FLAGS} -ldflags "$(LDFLAGS)" $(SOURCES) macos: ${SOURCES} $(GENERATED_CRDS) - GOOS=darwin GOARCH=amd64 CGO_ENABLED=${CGO_ENABLED} go build -o build/macos/${BINARY} ${BUILD_FLAGS} -ldflags "$(LDFLAGS)" $(SOURCES) + GOOS=darwin GOARCH=${GOARCH} CGO_ENABLED=${CGO_ENABLED} go build -o build/macos/${BINARY} ${BUILD_FLAGS} -ldflags "$(LDFLAGS)" $(SOURCES) docker: $(GENERATED_CRDS) ${DOCKERDIR}/${DOCKERFILE} echo `(env)` @@ -100,10 +101,10 @@ pooler: cd pooler; docker build --rm -t "$(POOLER_TAG)" --build-arg VERSION="${VERSION}" --build-arg BASE_IMAGE="${BASE_IMAGE}" . indocker-race: - docker run --rm -v "${GOPATH}":"${GOPATH}" -e GOPATH="${GOPATH}" -e RACE=1 -w ${PWD} golang:1.25.3 bash -c "make linux" + docker run --rm -v "${GOPATH}":"${GOPATH}" -e GOPATH="${GOPATH}" -e RACE=1 -w ${PWD} golang:1.26.4 bash -c "make linux" mocks: - GO111MODULE=on go generate ./... + go generate ./... fmt: @gofmt -l -w -s $(DIRS) @@ -113,7 +114,7 @@ vet: @staticcheck $(PKG) test: mocks $(GENERATED) $(GENERATED_CRDS) - GO111MODULE=on go test ./... + go test ./... codegen: $(GENERATED) diff --git a/README.md b/README.md index 9e5bc886b..31e9c1748 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ production for over five years. | Release | Postgres versions | K8s versions | Golang | | :-------- | :---------------: | :---------------: | :-----: | +| next | 14 → 18 | 1.27+ | 1.26.4 | | v1.15.1 | 13 → 17 | 1.27+ | 1.25.3 | | v1.14.0 | 13 → 17 | 1.27+ | 1.23.4 | | v1.13.0 | 12 → 16 | 1.27+ | 1.22.5 | diff --git a/docker/DebugDockerfile b/docker/DebugDockerfile index c44002984..367735d08 100644 --- a/docker/DebugDockerfile +++ b/docker/DebugDockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine +FROM golang:1.26-alpine LABEL maintainer="Team ACID @ Zalando " # We need root certificates to deal with teams api over https diff --git a/docker/Dockerfile b/docker/Dockerfile index 9eef4e68c..4ba9b7630 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,14 +1,19 @@ ARG BASE_IMAGE=alpine:latest -FROM golang:1.25-alpine AS builder + +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder ARG VERSION=latest +ARG TARGETOS +ARG TARGETARCH COPY . /go/src/github.com/zalando/postgres-operator WORKDIR /go/src/github.com/zalando/postgres-operator -RUN GO111MODULE=on go mod vendor \ - && CGO_ENABLED=0 go build -o build/postgres-operator -v -ldflags "-X=main.version=${VERSION}" cmd/main.go +RUN go clean -cache -modcache +RUN go mod vendor \ + && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o build/postgres-operator -v -ldflags "-X=main.version=${VERSION}" cmd/main.go -FROM ${BASE_IMAGE} +ARG BASE_IMAGE +FROM --platform=$TARGETPLATFORM ${BASE_IMAGE} LABEL maintainer="Team ACID @ Zalando " LABEL org.opencontainers.image.source="https://github.com/zalando/postgres-operator" diff --git a/docker/build_operator.sh b/docker/build_operator.sh index 5abe56666..0b1bf4d82 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.25.3.linux-${arch}.tar.gz" -O go.tar.gz + wget -q "https://storage.googleapis.com/golang/go1.26.4.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 @@ -26,5 +26,6 @@ export PATH="$PATH:$HOME/go/bin" export GOPATH="$HOME/go" mkdir -p build -GO111MODULE=on go mod vendor +go clean -cache -modcache +go mod vendor CGO_ENABLED=0 go build -o build/postgres-operator -v -ldflags "$OPERATOR_LDFLAGS" cmd/main.go diff --git a/e2e/Dockerfile b/e2e/Dockerfile index 98bbf755a..1f4083116 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -8,7 +8,7 @@ ENV TERM xterm-256color RUN apt-get -qq -y update \ # https://www.psycopg.org/docs/install.html#psycopg-vs-psycopg-binary && apt-get -qq -y install --no-install-recommends curl vim python3-dev \ - && curl -LO https://dl.k8s.io/release/v1.32.9/bin/linux/amd64/kubectl \ + && curl -LO https://dl.k8s.io/release/v1.36.1/bin/linux/amd64/kubectl \ && chmod +x ./kubectl \ && mv ./kubectl /usr/local/bin/kubectl \ && apt-get -qq -y clean \ diff --git a/e2e/Makefile b/e2e/Makefile index 5fa0de471..09ac74986 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.27.0 + cd "/tmp" && go install sigs.k8s.io/kind@v0.27.0 e2etest: tools copy clean ./run.sh main diff --git a/go.mod b/go.mod index 9efa24150..37c2d2b65 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/zalando/postgres-operator -go 1.25.3 +go 1.26.4 require ( - github.com/Masterminds/semver v1.5.0 - github.com/aws/aws-sdk-go v1.55.8 + github.com/Masterminds/semver/v3 v3.5.0 + github.com/aws/aws-sdk-go-v2 v1.42.0 + github.com/aws/aws-sdk-go-v2/config v1.32.24 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.305.3 github.com/golang/mock v1.6.0 github.com/lib/pq v1.12.3 github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d @@ -12,72 +14,80 @@ require ( github.com/r3labs/diff v1.1.0 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 - golang.org/x/crypto v0.45.0 - gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.32.9 - k8s.io/apiextensions-apiserver v0.32.9 - k8s.io/apimachinery v0.32.9 - k8s.io/client-go v0.32.9 - sigs.k8s.io/yaml v1.4.0 + golang.org/x/crypto v0.51.0 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.36.1 + k8s.io/apiextensions-apiserver v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 + sigs.k8s.io/yaml v1.6.0 ) require ( + github.com/aws/aws-sdk-go-v2/credentials v1.19.23 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gobuffalo/flect v1.0.3 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/spdystream v0.5.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.38.0 // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.45.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/code-generator v0.32.9 // indirect - k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/code-generator v0.36.1 // indirect + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/streaming v0.36.1 // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/controller-tools v0.17.3 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) tool ( diff --git a/go.sum b/go.sum index 0d0ebb7d2..c4927d42e 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,53 @@ -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= -github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/config v1.32.24 h1:aEDEj533yGdVvEHfkCY0D/1FbDrjnZr4pIulxRjqpHs= +github.com/aws/aws-sdk-go-v2/config v1.32.24/go.mod h1:yZtrGKJGlqfEW+/m2uTsJK+Jz7xF5R0eZfgcIG9m1ss= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23/go.mod h1:VsJF2ropPB37gDr7M2rLSpCE8IQWdpl62uae7qxZmqU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.305.3 h1:rEay0b3E0qqyY+W0c3ox8wGRsVK+GjxXadj9B9vpg4c= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.305.3/go.mod h1:8mrDF7OtbuL0QpwP4YCvLuoOE4/5lL7D33MXgp069/Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= @@ -28,42 +56,25 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -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.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -80,25 +91,22 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d h1:LznySqW8MqVeFh+pW6rOkFdld9QQ7jRydBKKM6jyPVI= github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d/go.mod h1:u3hJ0kqCQu/cPpsu3RbCOPZ0d7V3IjPjv1adNRleM9I= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -108,18 +116,20 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/r3labs/diff v1.1.0 h1:V53xhrbTHrWFWq3gI4b94AjgEJOerO1+1l0xyHOBi8M= github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6Xig= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -129,114 +139,105 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -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/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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-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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= -golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 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.32.9 h1:q/59kk8lnecgG0grJqzrmXC1Jcl2hPWp9ltz0FQuoLI= -k8s.io/api v0.32.9/go.mod h1:jIfT3rwW4EU1IXZm9qjzSk/2j91k4CJL5vUULrxqp3Y= -k8s.io/apiextensions-apiserver v0.32.9 h1:tpT1dUgWqEsTyrdoGckyw8OBASW1JfU08tHGaYBzFHY= -k8s.io/apiextensions-apiserver v0.32.9/go.mod h1:FoCi4zCLK67LNCCssFa2Wr9q4Xbvjx7MW4tdze5tpoA= -k8s.io/apimachinery v0.32.9 h1:fXk8ktfsxrdThaEOAQFgkhCK7iyoyvS8nbYJ83o/SSs= -k8s.io/apimachinery v0.32.9/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.9 h1:ZMyIQ1TEpTDAQni3L2gH1NZzyOA/gHfNcAazzCxMJ0c= -k8s.io/client-go v0.32.9/go.mod h1:2OT8aFSYvUjKGadaeT+AVbhkXQSpMAkiSb88Kz2WggI= -k8s.io/code-generator v0.32.9 h1:F9Gti/8I+nVNnQw02J36/YlSD5JMg4qDJ7sfRqpUICU= -k8s.io/code-generator v0.32.9/go.mod h1:fLYBG9g52EJulRebmomL0vCU0PQeMr7mnscfZtAAGV4= -k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4= -k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks= +k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/code-generator v0.36.1 h1:5bHQ7NbBcFFLHcoyo/hgU3m2tQV5RLz2nv4QNDlsbXc= +k8s.io/code-generator v0.36.1/go.mod h1:oCv8WmrW2RGdcMyvSk1aYbBfSs51ggtSFQr1YNeuAuo= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4= +k8s.io/streaming v0.36.1/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3EwfNw= sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 1363c2786..fa3efb599 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -34,7 +34,6 @@ GROUPS_WITH_VERSIONS="${CUSTOM_RESOURCE_NAME_ZAL}:${CUSTOM_RESOURCE_VERSION},${C echo "Generating deepcopy funcs" go tool deepcopy-gen \ --output-file zz_generated.deepcopy.go \ - --bounding-dirs "${APIS_PKG}" \ --go-header-file "${SCRIPT_ROOT}/hack/custom-boilerplate.go.txt" \ "${APIS_PKG}/${CUSTOM_RESOURCE_NAME_ZAL}/${CUSTOM_RESOURCE_VERSION}" \ "${APIS_PKG}/${CUSTOM_RESOURCE_NAME_ACID}/${CUSTOM_RESOURCE_VERSION}" diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 72d7153ba..9b855eb68 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -302,7 +302,9 @@ spec: a Container. properties: name: - description: Name of the environment variable. Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -360,6 +362,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -456,8 +495,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -515,6 +555,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -575,14 +652,14 @@ spec: envFrom: description: |- List of sources to populate environment variables in the container. - The keys defined within a source must be a C_IDENTIFIER. All invalid keys - will be reported as an event when the container is starting. When a key exists in multiple + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated. items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -603,8 +680,9 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. type: string secretRef: description: The Secret to select from @@ -867,6 +945,12 @@ spec: - port type: object type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string type: object livenessProbe: description: |- @@ -1237,7 +1321,9 @@ spec: type: integer type: object resizePolicy: - description: Resources resize policy for the container. + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. items: description: ContainerResizePolicy represents resource resize policy for the container. @@ -1269,7 +1355,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -1323,10 +1409,10 @@ spec: restartPolicy: description: |- RestartPolicy defines the restart behavior of individual containers in a pod. - This field may only be set for init containers, and the only allowed value is "Always". - For non-init containers or when this field is not specified, + This overrides the pod-level restart policy. When this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. - Setting the RestartPolicy as "Always" for the init container will have the following effect: + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy "Always" @@ -1338,6 +1424,59 @@ spec: init container is started, or after any startupProbe has successfully completed. type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic securityContext: description: |- SecurityContext defines the security options the container should be run with. @@ -1413,7 +1552,6 @@ spec: procMount denotes the type of proc mount to use for the containers. The default value is Default which uses the container runtime defaults for readonly paths and masked paths. - This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: @@ -1878,8 +2016,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1937,6 +2076,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1997,14 +2173,14 @@ spec: envFrom: description: |- List of sources to populate environment variables in the container. - The keys defined within a source must be a C_IDENTIFIER. All invalid keys - will be reported as an event when the container is starting. When a key exists in multiple + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated. items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -2025,8 +2201,9 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. type: string secretRef: description: The Secret to select from @@ -2289,6 +2466,12 @@ spec: - port type: object type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string type: object livenessProbe: description: |- @@ -2659,7 +2842,9 @@ spec: type: integer type: object resizePolicy: - description: Resources resize policy for the container. + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. items: description: ContainerResizePolicy represents resource resize policy for the container. @@ -2691,7 +2876,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -2745,10 +2930,10 @@ spec: restartPolicy: description: |- RestartPolicy defines the restart behavior of individual containers in a pod. - This field may only be set for init containers, and the only allowed value is "Always". - For non-init containers or when this field is not specified, + This overrides the pod-level restart policy. When this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. - Setting the RestartPolicy as "Always" for the init container will have the following effect: + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy "Always" @@ -2760,6 +2945,59 @@ spec: init container is started, or after any startupProbe has successfully completed. type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic securityContext: description: |- SecurityContext defines the security options the container should be run with. @@ -2835,7 +3073,6 @@ spec: procMount denotes the type of proc mount to use for the containers. The default value is Default which uses the container runtime defaults for readonly paths and masked paths. - This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: @@ -3849,8 +4086,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -3908,6 +4146,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4214,9 +4489,10 @@ spec: operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). type: string tolerationSeconds: description: |- @@ -4355,7 +4631,6 @@ spec: - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. If this value is nil, the behavior is equivalent to the Honor policy. - This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. type: string nodeTaintsPolicy: description: |- @@ -4366,7 +4641,6 @@ spec: - Ignore: node taints are ignored. All nodes are included. If this value is nil, the behavior is equivalent to the Ignore policy. - This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. type: string topologyKey: description: |- diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index 72d7153ba..9b855eb68 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -302,7 +302,9 @@ spec: a Container. properties: name: - description: Name of the environment variable. Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -360,6 +362,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -456,8 +495,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -515,6 +555,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -575,14 +652,14 @@ spec: envFrom: description: |- List of sources to populate environment variables in the container. - The keys defined within a source must be a C_IDENTIFIER. All invalid keys - will be reported as an event when the container is starting. When a key exists in multiple + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated. items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -603,8 +680,9 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. type: string secretRef: description: The Secret to select from @@ -867,6 +945,12 @@ spec: - port type: object type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string type: object livenessProbe: description: |- @@ -1237,7 +1321,9 @@ spec: type: integer type: object resizePolicy: - description: Resources resize policy for the container. + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. items: description: ContainerResizePolicy represents resource resize policy for the container. @@ -1269,7 +1355,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -1323,10 +1409,10 @@ spec: restartPolicy: description: |- RestartPolicy defines the restart behavior of individual containers in a pod. - This field may only be set for init containers, and the only allowed value is "Always". - For non-init containers or when this field is not specified, + This overrides the pod-level restart policy. When this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. - Setting the RestartPolicy as "Always" for the init container will have the following effect: + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy "Always" @@ -1338,6 +1424,59 @@ spec: init container is started, or after any startupProbe has successfully completed. type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic securityContext: description: |- SecurityContext defines the security options the container should be run with. @@ -1413,7 +1552,6 @@ spec: procMount denotes the type of proc mount to use for the containers. The default value is Default which uses the container runtime defaults for readonly paths and masked paths. - This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: @@ -1878,8 +2016,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1937,6 +2076,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1997,14 +2173,14 @@ spec: envFrom: description: |- List of sources to populate environment variables in the container. - The keys defined within a source must be a C_IDENTIFIER. All invalid keys - will be reported as an event when the container is starting. When a key exists in multiple + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated. items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -2025,8 +2201,9 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. type: string secretRef: description: The Secret to select from @@ -2289,6 +2466,12 @@ spec: - port type: object type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string type: object livenessProbe: description: |- @@ -2659,7 +2842,9 @@ spec: type: integer type: object resizePolicy: - description: Resources resize policy for the container. + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. items: description: ContainerResizePolicy represents resource resize policy for the container. @@ -2691,7 +2876,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -2745,10 +2930,10 @@ spec: restartPolicy: description: |- RestartPolicy defines the restart behavior of individual containers in a pod. - This field may only be set for init containers, and the only allowed value is "Always". - For non-init containers or when this field is not specified, + This overrides the pod-level restart policy. When this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. - Setting the RestartPolicy as "Always" for the init container will have the following effect: + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy "Always" @@ -2760,6 +2945,59 @@ spec: init container is started, or after any startupProbe has successfully completed. type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic securityContext: description: |- SecurityContext defines the security options the container should be run with. @@ -2835,7 +3073,6 @@ spec: procMount denotes the type of proc mount to use for the containers. The default value is Default which uses the container runtime defaults for readonly paths and masked paths. - This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: @@ -3849,8 +4086,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -3908,6 +4146,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4214,9 +4489,10 @@ spec: operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). type: string tolerationSeconds: description: |- @@ -4355,7 +4631,6 @@ spec: - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. If this value is nil, the behavior is equivalent to the Honor policy. - This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. type: string nodeTaintsPolicy: description: |- @@ -4366,7 +4641,6 @@ spec: - Ignore: node taints are ignored. All nodes are included. If this value is nil, the behavior is equivalent to the Ignore policy. - This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. type: string topologyKey: description: |- diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index a1e9f3c4f..eed4ee933 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -92,6 +92,7 @@ type Cluster struct { mu sync.Mutex userSyncStrategy spec.UserSyncer deleteOptions metav1.DeleteOptions + podEventsStore cache.Store podEventsQueue *cache.FIFO replicationSlots map[string]interface{} @@ -125,14 +126,17 @@ type compareLogicalBackupJobResult struct { func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgresql, logger *logrus.Entry, eventRecorder record.EventRecorder) *Cluster { deletePropagationPolicy := metav1.DeletePropagationOrphan - podEventsQueue := cache.NewFIFO(func(obj interface{}) (string, error) { + keyFn := func(obj interface{}) (string, error) { e, ok := obj.(PodEvent) if !ok { - return "", fmt.Errorf("could not cast to PodEvent") + return "", fmt.Errorf("could not cast to pod event") } return fmt.Sprintf("%s-%s", e.PodName, e.ResourceVersion), nil - }) + } + podEventsStore := cache.NewStore(keyFn) + podEventsQueue := cache.NewFIFO(keyFn) + passwordEncryption, ok := pgSpec.Spec.PostgresqlParam.Parameters["password_encryption"] if !ok { passwordEncryption = "scram-sha-256" @@ -159,6 +163,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres AdditionalOwnerRoles: cfg.OpConfig.AdditionalOwnerRoles, }, deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy}, + podEventsStore: podEventsStore, podEventsQueue: podEventsQueue, KubeClient: kubeClient, currentMajorVersion: 0, @@ -1356,6 +1361,9 @@ func (c *Cluster) NeedsRepair() (bool, acidv1.PostgresStatus) { // ReceivePodEvent is called back by the controller in order to add the cluster's pod event to the queue. func (c *Cluster) ReceivePodEvent(event PodEvent) { + if err := c.podEventsStore.Add(event); err != nil { + c.logger.Errorf("error when receiving pod event for lookup: %v", err) + } if err := c.podEventsQueue.Add(event); err != nil { c.logger.Errorf("error when receiving pod events: %v", err) } @@ -1396,7 +1404,19 @@ func (c *Cluster) processPodEventQueue(stopCh <-chan struct{}) { case <-stopCh: return default: - if _, err := c.podEventsQueue.Pop(cache.PopProcessFunc(c.processPodEvent)); err != nil { + _, err := c.podEventsQueue.Pop(cache.PopProcessFunc(func(obj interface{}, isInInitialList bool) error { + event, ok := obj.(PodEvent) + if !ok { + c.logger.Errorf("could not cast to pod event") + return nil // skip event to keep processing + } + c.processPodEvent(event, isInInitialList) + if err := c.podEventsStore.Delete(obj); err != nil { + c.logger.Errorf("failed to delete key from lookup store: %v", err) + } + return nil + })) + if err != nil { c.logger.Errorf("error when processing pod event queue %v", err) } } diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 5b668c108..306185e90 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -171,7 +171,7 @@ func (c *Cluster) enforceMinResourceLimits(resources *v1.ResourceRequirements) e msg = fmt.Sprintf("defined CPU limit %s for %q container is below required minimum %s and will be increased", cpuLimit.String(), constants.PostgresContainerName, minCPULimit) c.logger.Warningf("%s", msg) - c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", msg) + c.eventRecorder.Event(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", msg) resources.Limits[v1.ResourceCPU], _ = resource.ParseQuantity(minCPULimit) } } @@ -188,7 +188,7 @@ func (c *Cluster) enforceMinResourceLimits(resources *v1.ResourceRequirements) e msg = fmt.Sprintf("defined memory limit %s for %q container is below required minimum %s and will be increased", memoryLimit.String(), constants.PostgresContainerName, minMemoryLimit) c.logger.Warningf("%s", msg) - c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", msg) + c.eventRecorder.Event(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", msg) resources.Limits[v1.ResourceMemory], _ = resource.ParseQuantity(minMemoryLimit) } } @@ -1469,7 +1469,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef } sidecarContainers, conflicts := mergeContainers(clusterSpecificSidecars, c.Config.OpConfig.SidecarContainers, globalSidecarContainersByDockerImage, scalyrSidecars) - for containerName := range conflicts { + for _, containerName := range conflicts { c.logger.Warningf("a sidecar is specified twice. Ignoring sidecar %q in favor of %q with high a precedence", containerName, containerName) } diff --git a/pkg/cluster/majorversionupgrade.go b/pkg/cluster/majorversionupgrade.go index 6c754b14f..6995c50b5 100644 --- a/pkg/cluster/majorversionupgrade.go +++ b/pkg/cluster/majorversionupgrade.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/Masterminds/semver" + "github.com/Masterminds/semver/v3" "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util" v1 "k8s.io/api/core/v1" diff --git a/pkg/cluster/volumes.go b/pkg/cluster/volumes.go index 115474fef..e32e558e6 100644 --- a/pkg/cluster/volumes.go +++ b/pkg/cluster/volumes.go @@ -11,7 +11,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/filesystems" @@ -91,18 +91,18 @@ func (c *Cluster) syncUnderlyingEBSVolume() error { var modifyType *string if targetValue.Iops != nil && *targetValue.Iops >= int64(3000) { - if volume.Iops != *targetValue.Iops { + if volume.Iops != int64(*targetValue.Iops) { modifyIops = targetValue.Iops } } if targetValue.Throughput != nil && *targetValue.Throughput >= int64(125) { - if volume.Throughput != *targetValue.Throughput { + if volume.Throughput != int64(*targetValue.Throughput) { modifyThroughput = targetValue.Throughput } } - if targetSize > volume.Size { + if targetSize > int64(volume.Size) { modifySize = &targetSize } diff --git a/pkg/cluster/volumes_test.go b/pkg/cluster/volumes_test.go index 95ecc7624..d6472539b 100644 --- a/pkg/cluster/volumes_test.go +++ b/pkg/cluster/volumes_test.go @@ -11,7 +11,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 93a9c7f3e..879e44ad4 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -65,6 +65,7 @@ type Controller struct { nodesInformer cache.SharedIndexInformer podCh chan cluster.PodEvent + clusterEventStores []cache.Store // [workerID]Store clusterEventQueues []*cache.FIFO // [workerID]Queue lastClusterSyncTime int64 lastClusterRepairTime int64 @@ -356,17 +357,19 @@ func (c *Controller) initController() { c.config.InfrastructureRoles = infraRoles } + c.clusterEventStores = make([]cache.Store, c.opConfig.Workers) c.clusterEventQueues = make([]*cache.FIFO, c.opConfig.Workers) c.workerLogs = make(map[uint32]ringlog.RingLogger, c.opConfig.Workers) for i := range c.clusterEventQueues { - c.clusterEventQueues[i] = cache.NewFIFO(func(obj interface{}) (string, error) { + keyFn := func(obj interface{}) (string, error) { e, ok := obj.(ClusterEvent) if !ok { - return "", fmt.Errorf("could not cast to ClusterEvent") + return "", fmt.Errorf("could not cast to cluster event") } - return queueClusterKey(e.EventType, e.UID), nil - }) + } + c.clusterEventStores[i] = cache.NewStore(keyFn) + c.clusterEventQueues[i] = cache.NewFIFO(keyFn) } c.apiserver = apiserver.New(c, c.opConfig.APIPort, c.logger.Logger) diff --git a/pkg/controller/logs_and_api.go b/pkg/controller/logs_and_api.go index 4af5e1f36..24881f9d7 100644 --- a/pkg/controller/logs_and_api.go +++ b/pkg/controller/logs_and_api.go @@ -80,8 +80,8 @@ func (c *Controller) GetStatus() *spec.ControllerStatus { c.clustersMu.RUnlock() queueSizes := make(map[int]int, c.opConfig.Workers) - for workerID, queue := range c.clusterEventQueues { - queueSizes[workerID] = len(queue.ListKeys()) + for workerID, store := range c.clusterEventStores { + queueSizes[workerID] = len(store.ListKeys()) } return &spec.ControllerStatus{ @@ -180,11 +180,11 @@ func (c *Controller) Fire(e *logrus.Entry) error { // ListQueue dumps cluster event queue of the provided worker func (c *Controller) ListQueue(workerID uint32) (*spec.QueueDump, error) { - if workerID >= uint32(len(c.clusterEventQueues)) { + if workerID >= uint32(len(c.clusterEventStores)) { return nil, fmt.Errorf("could not find worker") } - q := c.clusterEventQueues[workerID] + q := c.clusterEventStores[workerID] return &spec.QueueDump{ Keys: q.ListKeys(), List: q.List(), @@ -196,7 +196,7 @@ func (c *Controller) GetWorkersCnt() uint32 { return c.opConfig.Workers } -//WorkerStatus provides status of the worker +// WorkerStatus provides status of the worker func (c *Controller) WorkerStatus(workerID uint32) (*cluster.WorkerStatus, error) { obj, ok := c.curWorkerCluster.Load(workerID) if !ok || obj == nil { diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index ab5e0d772..0933f7823 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -182,7 +182,7 @@ func (c *Controller) addCluster(lg *logrus.Entry, clusterName spec.NamespacedNam return cl, nil } -func (c *Controller) processEvent(event ClusterEvent) { +func (c *Controller) processEvent(event ClusterEvent, isInInitialList bool) { var clusterName spec.NamespacedName var clHistory ringlog.RingLogger var err error @@ -371,11 +371,22 @@ func (c *Controller) processClusterEventsQueue(idx int, stopCh <-chan struct{}, go func() { <-stopCh - c.clusterEventQueues[idx].Close() + (*c.clusterEventQueues[idx]).Close() }() for { - obj, err := c.clusterEventQueues[idx].Pop(cache.PopProcessFunc(func(interface{}, bool) error { return nil })) + _, err := (*c.clusterEventQueues[idx]).Pop(cache.PopProcessFunc(func(obj interface{}, isInitialList bool) error { + event, ok := obj.(ClusterEvent) + if !ok { + c.logger.Errorf("could not cast to cluster event") + return nil // skip event to keep processing + } + c.processEvent(event, isInitialList) + if err := c.clusterEventStores[idx].Delete(obj); err != nil { + c.logger.Errorf("failed to delete key from lookup store: %v", err) + } + return nil + })) if err != nil { if err == cache.ErrFIFOClosed { return @@ -383,12 +394,6 @@ func (c *Controller) processClusterEventsQueue(idx int, stopCh <-chan struct{}, c.logger.Errorf("error when processing cluster events queue: %v", err) continue } - event, ok := obj.(ClusterEvent) - if !ok { - c.logger.Errorf("could not cast to ClusterEvent") - } - - c.processEvent(event) } } @@ -523,7 +528,10 @@ func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1. } lg := c.logger.WithField("worker", workerID).WithField("cluster-name", clusterName) - if err := c.clusterEventQueues[workerID].Add(clusterEvent); err != nil { + if err := c.clusterEventStores[workerID].Add(clusterEvent); err != nil { + lg.Errorf("error while storing cluster event for lookup: %v", clusterEvent) + } + if err := (*c.clusterEventQueues[workerID]).Add(clusterEvent); err != nil { lg.Errorf("error while queueing cluster event: %v", clusterEvent) } lg.Infof("%s event has been queued", eventType) @@ -533,9 +541,9 @@ func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1. } // A delete event discards all prior requests for that cluster. for _, evType := range []EventType{EventAdd, EventSync, EventUpdate, EventRepair} { - obj, exists, err := c.clusterEventQueues[workerID].GetByKey(queueClusterKey(evType, uid)) + obj, exists, err := c.clusterEventStores[workerID].GetByKey(queueClusterKey(evType, uid)) if err != nil { - lg.Warningf("could not get event from the queue: %v", err) + lg.Warningf("could not get event from the lookup store: %v", err) continue } @@ -543,12 +551,18 @@ func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1. continue } - err = c.clusterEventQueues[workerID].Delete(obj) + err = (*c.clusterEventQueues[workerID]).Delete(obj) if err != nil { lg.Warningf("could not delete event from the queue: %v", err) } else { lg.Debugf("event %s has been discarded for the cluster", evType) } + err = c.clusterEventStores[workerID].Delete(obj) + if err != nil { + lg.Warningf("could not delete event from the lookup store: %v", err) + } else { + lg.Debugf("event %s has been deleted from the lookup store", evType) + } } } diff --git a/pkg/controller/util.go b/pkg/controller/util.go index f58c9df5a..6296e5341 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -16,7 +16,7 @@ import ( "github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/k8sutil" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) func (c *Controller) makeClusterConfig() cluster.Config { diff --git a/pkg/generated/clientset/versioned/fake/clientset_generated.go b/pkg/generated/clientset/versioned/fake/clientset_generated.go index 381ce23bd..f7f61ddea 100644 --- a/pkg/generated/clientset/versioned/fake/clientset_generated.go +++ b/pkg/generated/clientset/versioned/fake/clientset_generated.go @@ -30,6 +30,7 @@ import ( fakeacidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake" zalandov1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/zalando.org/v1" fakezalandov1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/zalando.org/v1/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" @@ -41,10 +42,6 @@ import ( // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. -// -// DEPRECATED: NewClientset replaces this with support for field management, which significantly improves -// server side apply testing. NewClientset is only available when apply configurations are generated (e.g. -// via --with-applyconfig). func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { @@ -57,9 +54,13 @@ func NewSimpleClientset(objects ...runtime.Object) *Clientset { cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + var opts metav1.ListOptions + if watchAction, ok := action.(testing.WatchActionImpl); ok { + opts = watchAction.ListOptions + } gvr := action.GetResource() ns := action.GetNamespace() - watch, err := o.Watch(gvr, ns) + watch, err := o.Watch(gvr, ns, opts) if err != nil { return false, nil, err } @@ -86,6 +87,17 @@ func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } +// IsWatchListSemanticsUnSupported informs the reflector that this client +// doesn't support WatchList semantics. +// +// This is a synthetic method whose sole purpose is to satisfy the optional +// interface check performed by the reflector. +// Returning true signals that WatchList can NOT be used. +// No additional logic is implemented here. +func (c *Clientset) IsWatchListSemanticsUnSupported() bool { + return true +} + var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} diff --git a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go index b53b029fb..93564ec3b 100644 --- a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go +++ b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go @@ -61,9 +61,7 @@ func (c *AcidV1Client) Postgresqls(namespace string) PostgresqlInterface { // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*AcidV1Client, error) { config := *c - if err := setConfigDefaults(&config); err != nil { - return nil, err - } + setConfigDefaults(&config) httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err @@ -75,9 +73,7 @@ func NewForConfig(c *rest.Config) (*AcidV1Client, error) { // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*AcidV1Client, error) { config := *c - if err := setConfigDefaults(&config); err != nil { - return nil, err - } + setConfigDefaults(&config) client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err @@ -100,7 +96,7 @@ func New(c rest.Interface) *AcidV1Client { return &AcidV1Client{c} } -func setConfigDefaults(config *rest.Config) error { +func setConfigDefaults(config *rest.Config) { gv := acidzalandov1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" @@ -109,8 +105,6 @@ func setConfigDefaults(config *rest.Config) error { if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } - - return nil } // RESTClient returns a RESTClient that is used to communicate diff --git a/pkg/generated/clientset/versioned/typed/zalando.org/v1/zalando.org_client.go b/pkg/generated/clientset/versioned/typed/zalando.org/v1/zalando.org_client.go index 99d795d76..803aee179 100644 --- a/pkg/generated/clientset/versioned/typed/zalando.org/v1/zalando.org_client.go +++ b/pkg/generated/clientset/versioned/typed/zalando.org/v1/zalando.org_client.go @@ -51,9 +51,7 @@ func (c *ZalandoV1Client) FabricEventStreams(namespace string) FabricEventStream // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*ZalandoV1Client, error) { config := *c - if err := setConfigDefaults(&config); err != nil { - return nil, err - } + setConfigDefaults(&config) httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err @@ -65,9 +63,7 @@ func NewForConfig(c *rest.Config) (*ZalandoV1Client, error) { // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ZalandoV1Client, error) { config := *c - if err := setConfigDefaults(&config); err != nil { - return nil, err - } + setConfigDefaults(&config) client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err @@ -90,7 +86,7 @@ func New(c rest.Interface) *ZalandoV1Client { return &ZalandoV1Client{c} } -func setConfigDefaults(config *rest.Config) error { +func setConfigDefaults(config *rest.Config) { gv := zalandoorgv1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" @@ -99,8 +95,6 @@ func setConfigDefaults(config *rest.Config) error { if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } - - return nil } // RESTClient returns a RESTClient that is used to communicate diff --git a/pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go b/pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go index 1522adc49..e2fee1a4e 100644 --- a/pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go +++ b/pkg/generated/informers/externalversions/acid.zalan.do/v1/operatorconfiguration.go @@ -34,6 +34,7 @@ import ( acidzalandov1 "github.com/zalando/postgres-operator/pkg/generated/listers/acid.zalan.do/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) @@ -55,36 +56,61 @@ type operatorConfigurationInformer struct { // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewOperatorConfigurationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredOperatorConfigurationInformer(client, namespace, resyncPeriod, indexers, nil) + return NewOperatorConfigurationInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) } // NewFilteredOperatorConfigurationInformer constructs a new informer for OperatorConfiguration type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredOperatorConfigurationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return NewOperatorConfigurationInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewOperatorConfigurationInformerWithOptions constructs a new informer for OperatorConfiguration type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewOperatorConfigurationInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + gvr := schema.GroupVersionResource{Group: "acid.zalan.do", Version: "v1", Resource: "operatorconfigurations"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.AcidV1().OperatorConfigurations(namespace).List(context.TODO(), options) + return client.AcidV1().OperatorConfigurations(namespace).List(context.Background(), opts) }, - WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.AcidV1().OperatorConfigurations(namespace).Watch(context.TODO(), options) + return client.AcidV1().OperatorConfigurations(namespace).Watch(context.Background(), opts) }, - }, + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.AcidV1().OperatorConfigurations(namespace).List(ctx, opts) + }, + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.AcidV1().OperatorConfigurations(namespace).Watch(ctx, opts) + }, + }, client), &apisacidzalandov1.OperatorConfiguration{}, - resyncPeriod, - indexers, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, ) } func (f *operatorConfigurationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredOperatorConfigurationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewOperatorConfigurationInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) } func (f *operatorConfigurationInformer) Informer() cache.SharedIndexInformer { diff --git a/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresql.go b/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresql.go index 1601f2bd6..c1b58ff05 100644 --- a/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresql.go +++ b/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresql.go @@ -34,6 +34,7 @@ import ( acidzalandov1 "github.com/zalando/postgres-operator/pkg/generated/listers/acid.zalan.do/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) @@ -55,36 +56,61 @@ type postgresqlInformer struct { // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewPostgresqlInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredPostgresqlInformer(client, namespace, resyncPeriod, indexers, nil) + return NewPostgresqlInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) } // NewFilteredPostgresqlInformer constructs a new informer for Postgresql type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredPostgresqlInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return NewPostgresqlInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewPostgresqlInformerWithOptions constructs a new informer for Postgresql type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPostgresqlInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + gvr := schema.GroupVersionResource{Group: "acid.zalan.do", Version: "v1", Resource: "postgresqls"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.AcidV1().Postgresqls(namespace).List(context.TODO(), options) + return client.AcidV1().Postgresqls(namespace).List(context.Background(), opts) }, - WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.AcidV1().Postgresqls(namespace).Watch(context.TODO(), options) + return client.AcidV1().Postgresqls(namespace).Watch(context.Background(), opts) }, - }, + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.AcidV1().Postgresqls(namespace).List(ctx, opts) + }, + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.AcidV1().Postgresqls(namespace).Watch(ctx, opts) + }, + }, client), &apisacidzalandov1.Postgresql{}, - resyncPeriod, - indexers, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, ) } func (f *postgresqlInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredPostgresqlInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewPostgresqlInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) } func (f *postgresqlInformer) Informer() cache.SharedIndexInformer { diff --git a/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresteam.go b/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresteam.go index b53862c78..954dfd19f 100644 --- a/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresteam.go +++ b/pkg/generated/informers/externalversions/acid.zalan.do/v1/postgresteam.go @@ -34,6 +34,7 @@ import ( acidzalandov1 "github.com/zalando/postgres-operator/pkg/generated/listers/acid.zalan.do/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) @@ -55,36 +56,61 @@ type postgresTeamInformer struct { // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewPostgresTeamInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredPostgresTeamInformer(client, namespace, resyncPeriod, indexers, nil) + return NewPostgresTeamInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) } // NewFilteredPostgresTeamInformer constructs a new informer for PostgresTeam type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredPostgresTeamInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return NewPostgresTeamInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewPostgresTeamInformerWithOptions constructs a new informer for PostgresTeam type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPostgresTeamInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + gvr := schema.GroupVersionResource{Group: "acid.zalan.do", Version: "v1", Resource: "postgresteams"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.AcidV1().PostgresTeams(namespace).List(context.TODO(), options) + return client.AcidV1().PostgresTeams(namespace).List(context.Background(), opts) }, - WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.AcidV1().PostgresTeams(namespace).Watch(context.TODO(), options) + return client.AcidV1().PostgresTeams(namespace).Watch(context.Background(), opts) }, - }, + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.AcidV1().PostgresTeams(namespace).List(ctx, opts) + }, + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.AcidV1().PostgresTeams(namespace).Watch(ctx, opts) + }, + }, client), &apisacidzalandov1.PostgresTeam{}, - resyncPeriod, - indexers, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, ) } func (f *postgresTeamInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredPostgresTeamInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewPostgresTeamInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) } func (f *postgresTeamInformer) Informer() cache.SharedIndexInformer { diff --git a/pkg/generated/informers/externalversions/factory.go b/pkg/generated/informers/externalversions/factory.go index d25563014..fb9ba76ac 100644 --- a/pkg/generated/informers/externalversions/factory.go +++ b/pkg/generated/informers/externalversions/factory.go @@ -25,6 +25,7 @@ SOFTWARE. package externalversions import ( + context "context" reflect "reflect" sync "sync" time "time" @@ -36,6 +37,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" + wait "k8s.io/apimachinery/pkg/util/wait" cache "k8s.io/client-go/tools/cache" ) @@ -50,6 +52,7 @@ type sharedInformerFactory struct { defaultResync time.Duration customResync map[reflect.Type]time.Duration transform cache.TransformFunc + informerName *cache.InformerName informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. @@ -96,6 +99,21 @@ func WithTransform(transform cache.TransformFunc) SharedInformerOption { } } +// WithInformerName sets the InformerName for informer identity used in metrics. +// The InformerName must be created via cache.NewInformerName() at startup, +// which validates global uniqueness. Each informer type will register its +// GVR under this name. +func WithInformerName(informerName *cache.InformerName) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.informerName = informerName + return factory + } +} + +func (f *sharedInformerFactory) InformerName() *cache.InformerName { + return f.informerName +} + // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync) @@ -104,6 +122,7 @@ func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Dur // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. // Listers obtained via this SharedInformerFactory will be subject to the same filters // as specified here. +// // Deprecated: Please use NewSharedInformerFactoryWithOptions instead func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) @@ -129,6 +148,10 @@ func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResy } func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.StartWithContext(wait.ContextForChannel(stopCh)) +} + +func (f *sharedInformerFactory) StartWithContext(ctx context.Context) { f.lock.Lock() defer f.lock.Unlock() @@ -138,15 +161,9 @@ func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { for informerType, informer := range f.informers { if !f.startedInformers[informerType] { - f.wg.Add(1) - // We need a new variable in each loop iteration, - // otherwise the goroutine would use the loop variable - // and that keeps changing. - informer := informer - go func() { - defer f.wg.Done() - informer.Run(stopCh) - }() + f.wg.Go(func() { + informer.RunWithContext(ctx) + }) f.startedInformers[informerType] = true } } @@ -159,9 +176,15 @@ func (f *sharedInformerFactory) Shutdown() { // Will return immediately if there is nothing to wait for. f.wg.Wait() + f.informerName.Release() } func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + result := f.WaitForCacheSyncWithContext(wait.ContextForChannel(stopCh)) + return result.Synced +} + +func (f *sharedInformerFactory) WaitForCacheSyncWithContext(ctx context.Context) cache.SyncResult { informers := func() map[reflect.Type]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() @@ -175,10 +198,31 @@ func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[ref return informers }() - res := map[reflect.Type]bool{} + // Wait for informers to sync, without polling. + cacheSyncs := make([]cache.DoneChecker, 0, len(informers)) + for _, informer := range informers { + cacheSyncs = append(cacheSyncs, informer.HasSyncedChecker()) + } + cache.WaitFor(ctx, "" /* no logging */, cacheSyncs...) + + res := cache.SyncResult{ + Synced: make(map[reflect.Type]bool, len(informers)), + } + failed := false for informType, informer := range informers { - res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + hasSynced := informer.HasSynced() + if !hasSynced { + failed = true + } + res.Synced[informType] = hasSynced } + if failed { + // context.Cause is more informative than ctx.Err(). + // This must be non-nil, otherwise WaitFor wouldn't have stopped + // prematurely. + res.Err = context.Cause(ctx) + } + return res } @@ -200,7 +244,9 @@ func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internal } informer = newFunc(f.client, resyncPeriod) - informer.SetTransform(f.transform) + if f.transform != nil { + informer.SetTransform(f.transform) + } f.informers[informerType] = informer return informer @@ -211,33 +257,52 @@ func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internal // // It is typically used like this: // -// ctx, cancel := context.Background() +// ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // factory := NewSharedInformerFactory(client, resyncPeriod) // defer factory.WaitForStop() // Returns immediately if nothing was started. // genericInformer := factory.ForResource(resource) // typedInformer := factory.SomeAPIGroup().V1().SomeType() -// factory.Start(ctx.Done()) // Start processing these informers. -// synced := factory.WaitForCacheSync(ctx.Done()) -// for v, ok := range synced { -// if !ok { -// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) -// return -// } +// handle, err := typeInformer.Informer().AddEventHandler(...) +// if err != nil { +// return fmt.Errorf("register event handler: %v", err) +// } +// defer typeInformer.Informer().RemoveEventHandler(handle) // Avoids leaking goroutines. +// factory.StartWithContext(ctx) // Start processing these informers. +// synced := factory.WaitForCacheSyncWithContext(ctx) +// if err := synced.AsError(); err != nil { +// return err +// } +// for v := range synced { +// // Only if desired log some information similar to this. +// fmt.Fprintf(os.Stdout, "cache synced: %s", v) +// } +// +// // Also make sure that all of the initial cache events have been delivered. +// if !WaitFor(ctx, "event handler sync", handle.HasSyncedChecker()) { +// // Must have failed because of context. +// return fmt.Errorf("sync event handler: %w", context.Cause(ctx)) // } // // // Creating informers can also be created after Start, but then // // Start must be called again: // anotherGenericInformer := factory.ForResource(resource) -// factory.Start(ctx.Done()) +// factory.StartWithContext(ctx) type SharedInformerFactory interface { internalinterfaces.SharedInformerFactory // Start initializes all requested informers. They are handled in goroutines // which run until the stop channel gets closed. // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + // + // Contextual logging: StartWithContext should be used instead of Start in code which supports contextual logging. Start(stopCh <-chan struct{}) + // StartWithContext initializes all requested informers. They are handled in goroutines + // which run until the context gets canceled. + // Warning: StartWithContext does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + StartWithContext(ctx context.Context) + // Shutdown marks a factory as shutting down. At that point no new // informers can be started anymore and Start will return without // doing anything. @@ -252,8 +317,14 @@ type SharedInformerFactory interface { // WaitForCacheSync blocks until all started informers' caches were synced // or the stop channel gets closed. + // + // Contextual logging: WaitForCacheSync should be used instead of WaitForCacheSync in code which supports contextual logging. It also returns a more useful result. WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + // WaitForCacheSyncWithContext blocks until all started informers' caches were synced + // or the context gets canceled. + WaitForCacheSyncWithContext(ctx context.Context) cache.SyncResult + // ForResource gives generic access to a shared informer of the matching type. ForResource(resource schema.GroupVersionResource) (GenericInformer, error) diff --git a/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go index 2037af01e..b8c3e7e51 100644 --- a/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -40,7 +40,26 @@ type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexI type SharedInformerFactory interface { Start(stopCh <-chan struct{}) InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer + InformerName() *cache.InformerName } // TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) + +// InformerOptions holds the options for creating an informer. +type InformerOptions struct { + // ResyncPeriod is the resync period for this informer. + // If not set, defaults to 0 (no resync). + ResyncPeriod time.Duration + + // Indexers are the indexers for this informer. + Indexers cache.Indexers + + // InformerName is used to uniquely identify this informer for metrics. + // If not set, metrics will not be published for this informer. + // Use cache.NewInformerName() to create an InformerName at startup. + InformerName *cache.InformerName + + // TweakListOptions is an optional function to modify the list options. + TweakListOptions TweakListOptionsFunc +} diff --git a/pkg/generated/informers/externalversions/zalando.org/v1/fabriceventstream.go b/pkg/generated/informers/externalversions/zalando.org/v1/fabriceventstream.go index 264ebb985..675058f80 100644 --- a/pkg/generated/informers/externalversions/zalando.org/v1/fabriceventstream.go +++ b/pkg/generated/informers/externalversions/zalando.org/v1/fabriceventstream.go @@ -34,6 +34,7 @@ import ( zalandoorgv1 "github.com/zalando/postgres-operator/pkg/generated/listers/zalando.org/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) @@ -55,36 +56,61 @@ type fabricEventStreamInformer struct { // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFabricEventStreamInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredFabricEventStreamInformer(client, namespace, resyncPeriod, indexers, nil) + return NewFabricEventStreamInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) } // NewFilteredFabricEventStreamInformer constructs a new informer for FabricEventStream type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredFabricEventStreamInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return NewFabricEventStreamInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewFabricEventStreamInformerWithOptions constructs a new informer for FabricEventStream type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFabricEventStreamInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + gvr := schema.GroupVersionResource{Group: "zalando.org", Version: "v1", Resource: "fabriceventstreams"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.ZalandoV1().FabricEventStreams(namespace).List(context.TODO(), options) + return client.ZalandoV1().FabricEventStreams(namespace).List(context.Background(), opts) }, - WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.ZalandoV1().FabricEventStreams(namespace).Watch(context.TODO(), options) + return client.ZalandoV1().FabricEventStreams(namespace).Watch(context.Background(), opts) }, - }, + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.ZalandoV1().FabricEventStreams(namespace).List(ctx, opts) + }, + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.ZalandoV1().FabricEventStreams(namespace).Watch(ctx, opts) + }, + }, client), &apiszalandoorgv1.FabricEventStream{}, - resyncPeriod, - indexers, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, ) } func (f *fabricEventStreamInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredFabricEventStreamInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewFabricEventStreamInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) } func (f *fabricEventStreamInformer) Informer() cache.SharedIndexInformer { diff --git a/pkg/util/volumes/ebs.go b/pkg/util/volumes/ebs.go index 45850d55f..bb7506d93 100644 --- a/pkg/util/volumes/ebs.go +++ b/pkg/util/volumes/ebs.go @@ -1,12 +1,13 @@ package volumes import ( + "context" "fmt" "strings" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" v1 "k8s.io/api/core/v1" "github.com/zalando/postgres-operator/pkg/util/constants" @@ -15,17 +16,17 @@ import ( // EBSVolumeResizer implements volume resizing interface for AWS EBS volumes. type EBSVolumeResizer struct { - connection *ec2.EC2 + connection *ec2.Client AWSRegion string } // ConnectToProvider connects to AWS. func (r *EBSVolumeResizer) ConnectToProvider() error { - sess, err := session.NewSession(&aws.Config{Region: aws.String(r.AWSRegion)}) + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(r.AWSRegion)) if err != nil { return fmt.Errorf("could not establish AWS session: %v", err) } - r.connection = ec2.New(sess) + r.connection = ec2.NewFromConfig(cfg) return nil } @@ -77,7 +78,7 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti } } - volumeOutput, err := r.connection.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIds: aws.StringSlice((volumeIds))}) + volumeOutput, err := r.connection.DescribeVolumes(context.TODO(), &ec2.DescribeVolumesInput{VolumeIds: volumeIds}) if err != nil { return nil, err } @@ -88,13 +89,13 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti } for _, v := range volumeOutput.Volumes { - switch *v.VolumeType { + switch v.VolumeType { case "gp3": - p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: *v.Size, VolumeType: *v.VolumeType, Iops: *v.Iops, Throughput: *v.Throughput}) + p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType), Iops: int64(*v.Iops), Throughput: int64(*v.Throughput)}) case "gp2": - p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: *v.Size, VolumeType: *v.VolumeType}) + p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType)}) default: - return nil, fmt.Errorf("discovered unexpected volume type %s %s", *v.VolumeId, *v.VolumeType) + return nil, fmt.Errorf("discovered unexpected volume type %s %s", *v.VolumeId, v.VolumeType) } } @@ -104,7 +105,7 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti // ResizeVolume actually calls AWS API to resize the EBS volume if necessary. func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { /* first check if the volume is already of a requested size */ - volumeOutput, err := r.connection.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIds: []*string{&volumeID}}) + volumeOutput, err := r.connection.DescribeVolumes(context.TODO(), &ec2.DescribeVolumesInput{VolumeIds: []string{volumeID}}) if err != nil { return fmt.Errorf("could not get information about the volume: %v", err) } @@ -112,17 +113,18 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { if *vol.VolumeId != volumeID { return fmt.Errorf("describe volume %q returned information about a non-matching volume %q", volumeID, *vol.VolumeId) } - if *vol.Size == newSize { + sizeInt32 := int32(newSize) + if *vol.Size == sizeInt32 { // nothing to do return nil } - input := ec2.ModifyVolumeInput{Size: &newSize, VolumeId: &volumeID} - output, err := r.connection.ModifyVolume(&input) + input := ec2.ModifyVolumeInput{Size: &sizeInt32, VolumeId: &volumeID} + output, err := r.connection.ModifyVolume(context.TODO(), &input) if err != nil { return fmt.Errorf("could not modify persistent volume: %v", err) } - state := *output.VolumeModification.ModificationState + state := output.VolumeModification.ModificationState if state == constants.EBSVolumeStateFailed { return fmt.Errorf("could not modify persistent volume %q: modification state failed", volumeID) } @@ -133,10 +135,10 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { return nil } // wait until the volume reaches the "optimizing" or "completed" state - in := ec2.DescribeVolumesModificationsInput{VolumeIds: []*string{&volumeID}} + in := ec2.DescribeVolumesModificationsInput{VolumeIds: []string{volumeID}} return retryutil.Retry(constants.EBSVolumeResizeWaitInterval, constants.EBSVolumeResizeWaitTimeout, func() (bool, error) { - out, err := r.connection.DescribeVolumesModifications(&in) + out, err := r.connection.DescribeVolumesModifications(context.TODO(), &in) if err != nil { return false, fmt.Errorf("could not describe volume modification: %v", err) } @@ -147,20 +149,35 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { return false, fmt.Errorf("non-matching volume id when describing modifications: %q is different from %q", *out.VolumesModifications[0].VolumeId, volumeID) } - return *out.VolumesModifications[0].ModificationState != constants.EBSVolumeStateModifying, nil + return out.VolumesModifications[0].ModificationState != constants.EBSVolumeStateModifying, nil }) } // ModifyVolume Modify EBS volume func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error { /* first check if the volume is already of a requested size */ - input := ec2.ModifyVolumeInput{Size: newSize, VolumeId: &volumeID, VolumeType: newType, Iops: iops, Throughput: throughput} - output, err := r.connection.ModifyVolume(&input) + var sizeInt32 *int32 + var iopsInt32 *int32 + var throughputInt32 *int32 + if newSize != nil { + s := int32(*newSize) + sizeInt32 = &s + } + if iops != nil { + i := int32(*iops) + iopsInt32 = &i + } + if throughput != nil { + t := int32(*throughput) + throughputInt32 = &t + } + input := ec2.ModifyVolumeInput{Size: sizeInt32, VolumeId: &volumeID, VolumeType: types.VolumeType(*newType), Iops: iopsInt32, Throughput: throughputInt32} + output, err := r.connection.ModifyVolume(context.TODO(), &input) if err != nil { return fmt.Errorf("could not modify persistent volume: %v", err) } - state := *output.VolumeModification.ModificationState + state := output.VolumeModification.ModificationState if state == constants.EBSVolumeStateFailed { return fmt.Errorf("could not modify persistent volume %q: modification state failed", volumeID) } @@ -171,10 +188,10 @@ func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSiz return nil } // wait until the volume reaches the "optimizing" or "completed" state - in := ec2.DescribeVolumesModificationsInput{VolumeIds: []*string{&volumeID}} + in := ec2.DescribeVolumesModificationsInput{VolumeIds: []string{volumeID}} return retryutil.Retry(constants.EBSVolumeResizeWaitInterval, constants.EBSVolumeResizeWaitTimeout, func() (bool, error) { - out, err := r.connection.DescribeVolumesModifications(&in) + out, err := r.connection.DescribeVolumesModifications(context.TODO(), &in) if err != nil { return false, fmt.Errorf("could not describe volume modification: %v", err) } @@ -185,7 +202,7 @@ func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSiz return false, fmt.Errorf("non-matching volume id when describing modifications: %q is different from %q", *out.VolumesModifications[0].VolumeId, volumeID) } - return *out.VolumesModifications[0].ModificationState != constants.EBSVolumeStateModifying, nil + return out.VolumesModifications[0].ModificationState != constants.EBSVolumeStateModifying, nil }) } From db0f112de62d5ae371b77f038640e657ece8552b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:34:25 +0200 Subject: [PATCH 35/70] Bump js-yaml from 4.1.1 to 4.2.0 in /ui/app (#3114) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/package.json b/ui/app/package.json index 7fd410bd7..7fa1ef373 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -38,7 +38,7 @@ "brfs": "^2.0.2", "dedent-js": "1.0.1", "eslint": "^8.32.0", - "js-yaml": "4.1.1", + "js-yaml": "4.2.0", "pug": "^3.0.2", "rimraf": "^4.1.2", "riot": "^3.13.2", From 49895a22009448641253c72a5ce08103e74f647b Mon Sep 17 00:00:00 2001 From: Pierre Ozoux Date: Fri, 26 Jun 2026 10:03:56 +0200 Subject: [PATCH 36/70] fix(helm): remove cpu limit (#2893) There is never a good case for cpu limits :) https://home.robusta.dev/blog/stop-using-cpu-limits Co-authored-by: Felix Kunde --- charts/postgres-operator/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index bb2831dd3..b880916f5 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -518,7 +518,6 @@ podPriorityClassName: resources: limits: - cpu: 500m memory: 500Mi requests: cpu: 100m From 26af5a7900dba58919d323f097eb6c9536eeae2d Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Fri, 26 Jun 2026 10:43:57 +0200 Subject: [PATCH 37/70] update docs about CRD generation and copy paste to chart dir, too (#3117) --- Makefile | 3 + .../crds/operatorconfigurations.yaml | 5 +- .../postgres-operator/crds/postgresqls.yaml | 343 ++++++++++++++++-- .../postgres-operator/crds/postgresteams.yaml | 86 +++-- docs/developer.md | 44 +-- manifests/operatorconfiguration.crd.yaml | 2 + manifests/postgresql.crd.yaml | 2 + manifests/postgresteam.crd.yaml | 2 + .../v1/operator_configuration_type.go | 1 + .../v1/operatorconfiguration.crd.yaml | 2 + .../acid.zalan.do/v1/postgres_team_type.go | 1 + pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 2 + pkg/apis/acid.zalan.do/v1/postgresql_type.go | 1 + 13 files changed, 395 insertions(+), 99 deletions(-) diff --git a/Makefile b/Makefile index 3613c1044..d85b74ed3 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,10 @@ $(GENERATED_CRDS): $(GENERATED) @mv manifests/acid.zalan.do_operatorconfigurations.yaml manifests/operatorconfiguration.crd.yaml @mv manifests/acid.zalan.do_postgresteams.yaml manifests/postgresteam.crd.yaml @cp manifests/postgresql.crd.yaml pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml + @cp manifests/postgresql.crd.yaml charts/postgres-operator/crds/postgresqls.yaml @cp manifests/operatorconfiguration.crd.yaml pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml + @cp manifests/operatorconfiguration.crd.yaml charts/postgres-operator/crds/operatorconfigurations.yaml + @cp manifests/postgresteam.crd.yaml charts/postgres-operator/crds/postgresteams.yaml local: ${SOURCES} $(GENERATED_CRDS) CGO_ENABLED=${CGO_ENABLED} go build -o build/${BINARY} $(LOCAL_BUILD_FLAGS) -ldflags "$(LDFLAGS)" $(SOURCES) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 5875b5808..a6d9a97d0 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -1,9 +1,12 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: operatorconfigurations.acid.zalan.do + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 labels: app.kubernetes.io/name: postgres-operator + name: operatorconfigurations.acid.zalan.do spec: group: acid.zalan.do names: diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 2bbf5ee49..cea738dec 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -1,9 +1,12 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: postgresqls.acid.zalan.do + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 labels: app.kubernetes.io/name: postgres-operator + name: postgresqls.acid.zalan.do spec: group: acid.zalan.do names: @@ -301,7 +304,9 @@ spec: a Container. properties: name: - description: Name of the environment variable. Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -359,6 +364,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -455,8 +497,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -514,6 +557,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -574,14 +654,14 @@ spec: envFrom: description: |- List of sources to populate environment variables in the container. - The keys defined within a source must be a C_IDENTIFIER. All invalid keys - will be reported as an event when the container is starting. When a key exists in multiple + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated. items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -602,8 +682,9 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. type: string secretRef: description: The Secret to select from @@ -866,6 +947,12 @@ spec: - port type: object type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string type: object livenessProbe: description: |- @@ -1236,7 +1323,9 @@ spec: type: integer type: object resizePolicy: - description: Resources resize policy for the container. + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. items: description: ContainerResizePolicy represents resource resize policy for the container. @@ -1268,7 +1357,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -1322,10 +1411,10 @@ spec: restartPolicy: description: |- RestartPolicy defines the restart behavior of individual containers in a pod. - This field may only be set for init containers, and the only allowed value is "Always". - For non-init containers or when this field is not specified, + This overrides the pod-level restart policy. When this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. - Setting the RestartPolicy as "Always" for the init container will have the following effect: + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy "Always" @@ -1337,6 +1426,59 @@ spec: init container is started, or after any startupProbe has successfully completed. type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic securityContext: description: |- SecurityContext defines the security options the container should be run with. @@ -1412,7 +1554,6 @@ spec: procMount denotes the type of proc mount to use for the containers. The default value is Default which uses the container runtime defaults for readonly paths and masked paths. - This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: @@ -1877,8 +2018,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1936,6 +2078,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1996,14 +2175,14 @@ spec: envFrom: description: |- List of sources to populate environment variables in the container. - The keys defined within a source must be a C_IDENTIFIER. All invalid keys - will be reported as an event when the container is starting. When a key exists in multiple + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated. items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -2024,8 +2203,9 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. type: string secretRef: description: The Secret to select from @@ -2288,6 +2468,12 @@ spec: - port type: object type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string type: object livenessProbe: description: |- @@ -2658,7 +2844,9 @@ spec: type: integer type: object resizePolicy: - description: Resources resize policy for the container. + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. items: description: ContainerResizePolicy represents resource resize policy for the container. @@ -2690,7 +2878,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -2744,10 +2932,10 @@ spec: restartPolicy: description: |- RestartPolicy defines the restart behavior of individual containers in a pod. - This field may only be set for init containers, and the only allowed value is "Always". - For non-init containers or when this field is not specified, + This overrides the pod-level restart policy. When this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. - Setting the RestartPolicy as "Always" for the init container will have the following effect: + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy "Always" @@ -2759,6 +2947,59 @@ spec: init container is started, or after any startupProbe has successfully completed. type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic securityContext: description: |- SecurityContext defines the security options the container should be run with. @@ -2834,7 +3075,6 @@ spec: procMount denotes the type of proc mount to use for the containers. The default value is Default which uses the container runtime defaults for readonly paths and masked paths. - This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: @@ -3848,8 +4088,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be - a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -3907,6 +4148,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4213,9 +4491,10 @@ spec: operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). type: string tolerationSeconds: description: |- @@ -4354,7 +4633,6 @@ spec: - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. If this value is nil, the behavior is equivalent to the Honor policy. - This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. type: string nodeTaintsPolicy: description: |- @@ -4365,7 +4643,6 @@ spec: - Ignore: node taints are ignored. All nodes are included. If this value is nil, the behavior is equivalent to the Ignore policy. - This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. type: string topologyKey: description: |- diff --git a/charts/postgres-operator/crds/postgresteams.yaml b/charts/postgres-operator/crds/postgresteams.yaml index b7a36848d..3bc7fcd1d 100644 --- a/charts/postgres-operator/crds/postgresteams.yaml +++ b/charts/postgres-operator/crds/postgresteams.yaml @@ -1,70 +1,84 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: postgresteams.acid.zalan.do + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 labels: app.kubernetes.io/name: postgres-operator + name: postgresteams.acid.zalan.do spec: group: acid.zalan.do names: + categories: + - all kind: PostgresTeam listKind: PostgresTeamList plural: postgresteams - singular: postgresteam shortNames: - pgteam - categories: - - all + singular: postgresteam scope: Namespaced versions: - name: v1 - served: true - storage: true - subresources: - status: {} schema: openAPIV3Schema: - type: object - required: - - kind - - apiVersion - - spec + description: PostgresTeam defines Custom Resource Definition Object for team + management. properties: - kind: - type: string - enum: - - PostgresTeam apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - enum: - - acid.zalan.do/v1 - spec: + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: type: object + spec: + description: PostgresTeamSpec defines the specification for the PostgresTeam + TPR. properties: - additionalSuperuserTeams: - type: object - description: "Map for teamId and associated additional superuser teams" + additionalMembers: additionalProperties: - type: array - nullable: true - description: "List of teams to become Postgres superusers" + description: List of users who will also be added to the Postgres + cluster. items: type: string - additionalTeams: + type: array + description: Map for teamId and associated additional users type: object - description: "Map for teamId and associated additional teams" + additionalSuperuserTeams: additionalProperties: - type: array - nullable: true - description: "List of teams whose members will also be added to the Postgres cluster" + description: List of teams to become Postgres superusers items: type: string - additionalMembers: + type: array + description: Map for teamId and associated additional superuser teams type: object - description: "Map for teamId and associated additional users" + additionalTeams: additionalProperties: - type: array - nullable: true - description: "List of users who will also be added to the Postgres cluster" + description: List of teams whose members will also be added to the + Postgres cluster. items: type: string + type: array + description: Map for teamId and associated additional teams + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/docs/developer.md b/docs/developer.md index 141ee63de..eb7cb0e9f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -27,20 +27,11 @@ git clone https://github.com/zalando/postgres-operator.git ## Building the operator -We use [Go Modules](https://github.com/golang/go/wiki/Modules) for handling -dependencies. When using Go below v1.13 you need to explicitly enable Go modules -by setting the `GO111MODULE` environment variable to `on`. The make targets do -this for you, so simply run +We use [Go Modules](https://github.com/golang/go/wiki/Modules) for handling dependencies. +Run `go mod vendor && go mod tidy` to install them. -```bash -make -``` - -Build the operator with the `make docker` command. You may define the TAG -variable to assign an explicit tag to your Docker image and the IMAGE to set -the image name. By default, the tag is computed with -`git describe --tags --always --dirty` and the image is -`ghcr.io/zalando/postgres-operator` +Build the operator with the `make docker` command. You may define the TAG variable to assign an explicit tag to your Docker image and the IMAGE to set the image name. By default, the tag is computed with +`git describe --tags --always --dirty` and the image is `ghcr.io/zalando/postgres-operator`. On macos search and replace `sed -i` commands with `sed -i ''` for the make commands to work. ```bash export TAG=$(git describe --tags --always --dirty) @@ -296,8 +287,7 @@ Please run flake8 [before submitting a PR](http://flake8.pycqa.org/en/latest/use In the case you want to add functionality to the operator that shall be controlled via the operator configuration there are a few places that need to be updated. As explained [here](reference/operator_parameters.md), it's possible -to configure the operator either with a ConfigMap or CRD, but currently we aim -to synchronize parameters everywhere. +to configure the operator either with a ConfigMap or CRD. When choosing a parameter name for a new option in a Postgres cluster manifest, keep in mind the naming conventions there. We use `camelCase` for manifest @@ -320,32 +310,28 @@ manifest files: Postgres manifest parameters are defined in the [api package](https://github.com/zalando/postgres-operator/blob/master/pkg/apis/acid.zalan.do/v1/postgresql_type.go). The operator behavior has to be implemented at least in [k8sres.go](https://github.com/zalando/postgres-operator/blob/master/pkg/cluster/k8sres.go). -Validation of CRD parameters is controlled in [crds.go](https://github.com/zalando/postgres-operator/blob/master/pkg/apis/acid.zalan.do/v1/crds.go). Please, reflect your changes in tests, for example in: * [config_test.go](https://github.com/zalando/postgres-operator/blob/master/pkg/util/config/config_test.go) * [k8sres_test.go](https://github.com/zalando/postgres-operator/blob/master/pkg/cluster/k8sres_test.go) * [util_test.go](https://github.com/zalando/postgres-operator/blob/master/pkg/apis/acid.zalan.do/v1/util_test.go) -### Updating manifest files +### Generating the CRDs -For the CRD-based configuration, please update the following files: - -* the default [OperatorConfiguration](https://github.com/zalando/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml) -* the CRD's [validation](https://github.com/zalando/postgres-operator/blob/master/manifests/operatorconfiguration.crd.yaml) -* the CRD's validation in the [Helm chart](https://github.com/zalando/postgres-operator/blob/master/charts/postgres-operator/crds/operatorconfigurations.yaml) - -Add new options also to the Helm chart's [values file](https://github.com/zalando/postgres-operator/blob/master/charts/postgres-operator/values.yaml) file. -It follows the OperatorConfiguration CRD layout. Nested values will be flattened for the ConfigMap. -Last but no least, update the [ConfigMap](https://github.com/zalando/postgres-operator/blob/master/manifests/configmap.yaml) manifest example as well. +The CRDs can be automatically generated from the go structs. Use the correct kubebuilder annotations for defining the validation, constraints or default values etc.. Run `make` to update the CRDs which are stored in three locations: +- In the Go api package +- The example manifests folder +- The helm chart folder ### Updating documentation -Finally, add a section for each new configuration option and/or cluster manifest +Config changes need to be reflected in the Helm chart's [values file](https://github.com/zalando/postgres-operator/blob/master/charts/postgres-operator/values.yaml), too. It follows the OperatorConfiguration CRD layout. Nested values will be flattened for the ConfigMap. + +Add a section for each new configuration option and/or cluster manifest parameter in the reference documents: * [config reference](reference/operator_parameters.md) * [manifest reference](reference/cluster_manifest.md) -It also helps users to explain new features with examples in the -[administrator docs](administrator.md). +It can also help other K8s admins to explain new features with examples in the +[administrator docs](administrator.md) and also update the [OperatorConfiguration CRD](https://github.com/zalando/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml) and [ConfigMap](https://github.com/zalando/postgres-operator/blob/master/manifests/configmap.yaml) manifest examples. diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 5f347f2ac..a6d9a97d0 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -4,6 +4,8 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.17.3 + labels: + app.kubernetes.io/name: postgres-operator name: operatorconfigurations.acid.zalan.do spec: group: acid.zalan.do diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 9b855eb68..cea738dec 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -4,6 +4,8 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.17.3 + labels: + app.kubernetes.io/name: postgres-operator name: postgresqls.acid.zalan.do spec: group: acid.zalan.do diff --git a/manifests/postgresteam.crd.yaml b/manifests/postgresteam.crd.yaml index 2245c6253..3bc7fcd1d 100644 --- a/manifests/postgresteam.crd.yaml +++ b/manifests/postgresteam.crd.yaml @@ -4,6 +4,8 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.17.3 + labels: + app.kubernetes.io/name: postgres-operator name: postgresteams.acid.zalan.do spec: group: acid.zalan.do 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 60793c45c..a55fdd7a8 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -24,6 +24,7 @@ import ( // +kubebuilder:printcolumn:name="Min-Instances",type=integer,JSONPath=`.configuration.min_instances`,description="Minimum number of instances per Postgres cluster" // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age of the OperatorConfiguration resource" // +kubebuilder:subresource:status +// +kubebuilder:metadata:labels=app.kubernetes.io/name=postgres-operator type OperatorConfiguration struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata"` diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 5f347f2ac..a6d9a97d0 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -4,6 +4,8 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.17.3 + labels: + app.kubernetes.io/name: postgres-operator name: operatorconfigurations.acid.zalan.do spec: group: acid.zalan.do diff --git a/pkg/apis/acid.zalan.do/v1/postgres_team_type.go b/pkg/apis/acid.zalan.do/v1/postgres_team_type.go index ffedaef57..be39dea07 100644 --- a/pkg/apis/acid.zalan.do/v1/postgres_team_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgres_team_type.go @@ -11,6 +11,7 @@ import ( // +k8s:deepcopy-gen=true // +kubebuilder:resource:shortName=pgteam,categories=all // +kubebuilder:subresource:status +// +kubebuilder:metadata:labels=app.kubernetes.io/name=postgres-operator type PostgresTeam struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata"` diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index 9b855eb68..cea738dec 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -4,6 +4,8 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.17.3 + labels: + app.kubernetes.io/name: postgres-operator name: postgresqls.acid.zalan.do spec: group: acid.zalan.do diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 81efa42cc..85a73605c 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -24,6 +24,7 @@ import ( // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age of the PostgreSQL cluster" // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.PostgresClusterStatus`,description="Current sync status of postgresql resource" // +kubebuilder:subresource:status +// +kubebuilder:metadata:labels=app.kubernetes.io/name=postgres-operator type Postgresql struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata"` From 9030520e24006392846fc7725f5da0aa20ed362f Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Fri, 26 Jun 2026 17:02:50 +0200 Subject: [PATCH 38/70] fix array validation for global maintenance window and protected role names (#3120) * change type of maintenance windows in config crd * fix default array type for protected role names --- .../crds/operatorconfigurations.yaml | 8 +++++++- manifests/operatorconfiguration.crd.yaml | 8 +++++++- .../acid.zalan.do/v1/operator_configuration_type.go | 8 ++++---- .../acid.zalan.do/v1/operatorconfiguration.crd.yaml | 8 +++++++- pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go | 6 ++---- pkg/controller/operator_config.go | 11 +---------- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index a6d9a97d0..19d957431 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -706,6 +706,10 @@ spec: type: integer type: object maintenance_windows: + items: + 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))\ + *$ + type: string type: array major_version_upgrade: description: MajorVersionUpgradeConfiguration defines how to execute @@ -851,7 +855,9 @@ spec: type: string type: array protected_role_names: - default: '["admin", "cron_admin"]' + default: + - admin + - cron_admin items: type: string type: array diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index a6d9a97d0..19d957431 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -706,6 +706,10 @@ spec: type: integer type: object maintenance_windows: + items: + 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))\ + *$ + type: string type: array major_version_upgrade: description: MajorVersionUpgradeConfiguration defines how to execute @@ -851,7 +855,9 @@ spec: type: string type: array protected_role_names: - default: '["admin", "cron_admin"]' + default: + - admin + - cron_admin items: type: string type: array 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 a55fdd7a8..0d98d487a 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -284,7 +284,7 @@ type TeamsAPIConfiguration struct { PamRoleName string `json:"pam_role_name,omitempty"` // +kubebuilder:default="https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees" PamConfiguration string `json:"pam_configuration,omitempty"` - // +kubebuilder:default="[\"admin\", \"cron_admin\"]" + // +kubebuilder:default={"admin", "cron_admin"} ProtectedRoles []string `json:"protected_role_names,omitempty"` PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"` // +kubebuilder:default=true @@ -426,10 +426,10 @@ type OperatorConfigurationData struct { RepairPeriod Duration `json:"repair_period,omitempty"` // +kubebuilder:default=true EnableMaintenanceWindows *bool `json:"enable_maintenance_windows,omitempty"` - // +kubebuilder:validation:Schemaless // +kubebuilder:validation:Type=array - MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"` - SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"` + // +kubebuilder:validation:items: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))\ *$` + MaintenanceWindows []string `json:"maintenance_windows,omitempty"` + SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"` // +kubebuilder:default=true ShmVolume *bool `json:"enable_shm_volume,omitempty"` SidecarImages map[string]string `json:"sidecar_docker_images,omitempty"` // deprecated in favour of SidecarContainers diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index a6d9a97d0..19d957431 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -706,6 +706,10 @@ spec: type: integer type: object maintenance_windows: + items: + 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))\ + *$ + type: string type: array major_version_upgrade: description: MajorVersionUpgradeConfiguration defines how to execute @@ -851,7 +855,9 @@ spec: type: string type: array protected_role_names: - default: '["admin", "cron_admin"]' + default: + - admin + - cron_admin items: type: string type: array 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 7d18c2cf2..4fd9c4a00 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -440,10 +440,8 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData } if in.MaintenanceWindows != nil { in, out := &in.MaintenanceWindows, &out.MaintenanceWindows - *out = make([]MaintenanceWindow, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + *out = make([]string, len(*in)) + copy(*out, *in) } if in.ShmVolume != nil { in, out := &in.ShmVolume, &out.ShmVolume diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 66fc7a731..94ef86d7c 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -51,16 +51,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.SidecarImages = fromCRD.SidecarImages result.SidecarContainers = fromCRD.SidecarContainers result.EnableMaintenanceWindows = util.CoalesceBool(fromCRD.EnableMaintenanceWindows, util.True()) - if len(fromCRD.MaintenanceWindows) > 0 { - result.MaintenanceWindows = make([]string, 0, len(fromCRD.MaintenanceWindows)) - for _, window := range fromCRD.MaintenanceWindows { - w, err := window.MarshalJSON() - if err != nil { - panic(fmt.Errorf("could not marshal configured maintenance window: %v", err)) - } - result.MaintenanceWindows = append(result.MaintenanceWindows, string(w)) - } - } + result.MaintenanceWindows = fromCRD.MaintenanceWindows // user config result.SuperUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.SuperUsername, "postgres") From 1227fc2e1c6a4b51b83ad9696bf69fd3e29e66bc Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Mon, 29 Jun 2026 10:32:24 +0200 Subject: [PATCH 39/70] switch to metav1.Duration for timeout options (#3121) * switch to metav1.Duration for timeout options * fix custom config parsing and use safe coalesce --- .../crds/operatorconfigurations.yaml | 36 ++++------- manifests/operatorconfiguration.crd.yaml | 36 ++++------- pkg/apis/acid.zalan.do/v1/marshal.go | 27 -------- .../v1/operator_configuration_type.go | 33 +++++----- .../v1/operatorconfiguration.crd.yaml | 36 ++++------- .../acid.zalan.do/v1/zz_generated.deepcopy.go | 62 ++++++++++++++++++- pkg/cluster/cluster_test.go | 7 ++- pkg/cluster/connection_pooler.go | 2 +- pkg/cluster/k8sres.go | 6 +- pkg/cluster/k8sres_test.go | 29 +++++---- pkg/cluster/pod.go | 6 +- pkg/cluster/pod_test.go | 5 +- pkg/cluster/resources.go | 6 +- pkg/cluster/sync_test.go | 24 +++---- pkg/cluster/util.go | 8 +-- pkg/cluster/util_test.go | 8 +-- pkg/controller/node.go | 2 +- pkg/controller/operator_config.go | 26 ++++---- pkg/controller/postgresql.go | 2 +- pkg/util/config/config.go | 32 +++++----- pkg/util/config/config_test.go | 12 ++-- pkg/util/config/util.go | 19 +++--- pkg/util/util.go | 8 +-- 23 files changed, 214 insertions(+), 218 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 19d957431..9cd6b27b7 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -447,8 +447,7 @@ spec: default: 20m description: timeout for successful migration of master pods from unschedulable node - format: int64 - type: integer + type: string node_readiness_label: additionalProperties: type: string @@ -521,8 +520,7 @@ spec: default: 5m description: Postgres pods are terminated forcefully after this timeout - format: int64 - type: integer + type: string secret_name_template: default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' description: |- @@ -783,13 +781,11 @@ spec: repair_period: default: 5m description: period between consecutive repair requests - format: int64 - type: integer + type: string resync_period: default: 30m description: period between consecutive sync requests - format: int64 - type: integer + type: string scalyr: description: ScalyrConfiguration defines the configuration for ScalyrAPI properties: @@ -885,48 +881,40 @@ spec: default: 1s description: interval between consecutive attempts of operator calling the Patroni API - format: int64 - type: integer + type: string patroni_api_check_timeout: default: 5s description: timeout when waiting for successful response from Patroni API - format: int64 - type: integer + type: string pod_deletion_wait_timeout: default: 10m description: timeout when waiting for the Postgres pods to be deleted - format: int64 - type: integer + type: string pod_label_wait_timeout: default: 10m description: timeout when waiting for pod role and cluster labels - format: int64 - type: integer + type: string ready_wait_interval: default: 4s description: interval between consecutive attempts waiting for postgresql CRD to be created - format: int64 - type: integer + type: string ready_wait_timeout: default: 30s description: timeout for the complete postgres CRD creation - format: int64 - type: integer + type: string resource_check_interval: default: 3s description: interval to wait between consecutive attempts to check for some K8s resources - format: int64 - type: integer + type: string resource_check_timeout: default: 10m description: timeout when waiting for the presence of a certain K8s resource - format: int64 - type: integer + type: string type: object users: description: PostgresUsersConfiguration defines the system users of diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 19d957431..9cd6b27b7 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -447,8 +447,7 @@ spec: default: 20m description: timeout for successful migration of master pods from unschedulable node - format: int64 - type: integer + type: string node_readiness_label: additionalProperties: type: string @@ -521,8 +520,7 @@ spec: default: 5m description: Postgres pods are terminated forcefully after this timeout - format: int64 - type: integer + type: string secret_name_template: default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' description: |- @@ -783,13 +781,11 @@ spec: repair_period: default: 5m description: period between consecutive repair requests - format: int64 - type: integer + type: string resync_period: default: 30m description: period between consecutive sync requests - format: int64 - type: integer + type: string scalyr: description: ScalyrConfiguration defines the configuration for ScalyrAPI properties: @@ -885,48 +881,40 @@ spec: default: 1s description: interval between consecutive attempts of operator calling the Patroni API - format: int64 - type: integer + type: string patroni_api_check_timeout: default: 5s description: timeout when waiting for successful response from Patroni API - format: int64 - type: integer + type: string pod_deletion_wait_timeout: default: 10m description: timeout when waiting for the Postgres pods to be deleted - format: int64 - type: integer + type: string pod_label_wait_timeout: default: 10m description: timeout when waiting for pod role and cluster labels - format: int64 - type: integer + type: string ready_wait_interval: default: 4s description: interval between consecutive attempts waiting for postgresql CRD to be created - format: int64 - type: integer + type: string ready_wait_timeout: default: 30s description: timeout for the complete postgres CRD creation - format: int64 - type: integer + type: string resource_check_interval: default: 3s description: interval to wait between consecutive attempts to check for some K8s resources - format: int64 - type: integer + type: string resource_check_timeout: default: 10m description: timeout when waiting for the presence of a certain K8s resource - format: int64 - type: integer + type: string type: object users: description: PostgresUsersConfiguration defines the system users of diff --git a/pkg/apis/acid.zalan.do/v1/marshal.go b/pkg/apis/acid.zalan.do/v1/marshal.go index 014214fed..ac351bc0d 100644 --- a/pkg/apis/acid.zalan.do/v1/marshal.go +++ b/pkg/apis/acid.zalan.do/v1/marshal.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "strings" - "time" ) type postgresqlCopy Postgresql @@ -120,29 +119,3 @@ func (p *Postgresql) UnmarshalJSON(data []byte) error { return nil } - -// UnmarshalJSON convert to Duration from byte slice of json -func (d *Duration) UnmarshalJSON(b []byte) error { - var ( - v interface{} - err error - ) - if err = json.Unmarshal(b, &v); err != nil { - return err - } - switch val := v.(type) { - case string: - t, err := time.ParseDuration(val) - if err != nil { - return err - } - *d = Duration(t) - return nil - case float64: - t := time.Duration(val) - *d = Duration(t) - return nil - default: - return fmt.Errorf("could not recognize type %T as a valid type to unmarshal to Duration", val) - } -} 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 0d98d487a..27063fd23 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -5,8 +5,6 @@ package v1 import ( "github.com/zalando/postgres-operator/pkg/util/config" - "time" - "github.com/zalando/postgres-operator/pkg/spec" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -78,7 +76,7 @@ type KubernetesMetaConfiguration struct { PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"` // +kubebuilder:default="5m" // Postgres pods are terminated forcefully after this timeout - PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"` + PodTerminateGracePeriod *metav1.Duration `json:"pod_terminate_grace_period,omitempty"` // +optional LivenessProbe *v1.Probe `json:"liveness_probe"` SpiloPrivileged bool `json:"spilo_privileged,omitempty"` @@ -143,9 +141,9 @@ type KubernetesMetaConfiguration struct { PodPriorityClassName string `json:"pod_priority_class_name,omitempty"` // +kubebuilder:default="20m" // timeout for successful migration of master pods from unschedulable node - MasterPodMoveTimeout Duration `json:"master_pod_move_timeout,omitempty"` - EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"` - PodAntiAffinityPreferredDuringScheduling bool `json:"pod_antiaffinity_preferred_during_scheduling,omitempty"` + MasterPodMoveTimeout *metav1.Duration `json:"master_pod_move_timeout,omitempty"` + EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"` + PodAntiAffinityPreferredDuringScheduling bool `json:"pod_antiaffinity_preferred_during_scheduling,omitempty"` // +kubebuilder:default="kubernetes.io/hostname" PodAntiAffinityTopologyKey string `json:"pod_antiaffinity_topology_key,omitempty"` // +kubebuilder:validation:Enum=ordered_ready;parallel @@ -186,28 +184,28 @@ type PostgresPodResourcesDefaults struct { type OperatorTimeouts struct { // +kubebuilder:default="3s" // interval to wait between consecutive attempts to check for some K8s resources - ResourceCheckInterval Duration `json:"resource_check_interval,omitempty"` + ResourceCheckInterval *metav1.Duration `json:"resource_check_interval,omitempty"` // +kubebuilder:default="10m" // timeout when waiting for the presence of a certain K8s resource - ResourceCheckTimeout Duration `json:"resource_check_timeout,omitempty"` + ResourceCheckTimeout *metav1.Duration `json:"resource_check_timeout,omitempty"` // +kubebuilder:default="10m" // timeout when waiting for pod role and cluster labels - PodLabelWaitTimeout Duration `json:"pod_label_wait_timeout,omitempty"` + PodLabelWaitTimeout *metav1.Duration `json:"pod_label_wait_timeout,omitempty"` // +kubebuilder:default="10m" // timeout when waiting for the Postgres pods to be deleted - PodDeletionWaitTimeout Duration `json:"pod_deletion_wait_timeout,omitempty"` + PodDeletionWaitTimeout *metav1.Duration `json:"pod_deletion_wait_timeout,omitempty"` // +kubebuilder:default="4s" // interval between consecutive attempts waiting for postgresql CRD to be created - ReadyWaitInterval Duration `json:"ready_wait_interval,omitempty"` + ReadyWaitInterval *metav1.Duration `json:"ready_wait_interval,omitempty"` // +kubebuilder:default="30s" // timeout for the complete postgres CRD creation - ReadyWaitTimeout Duration `json:"ready_wait_timeout,omitempty"` + ReadyWaitTimeout *metav1.Duration `json:"ready_wait_timeout,omitempty"` // +kubebuilder:default="1s" // interval between consecutive attempts of operator calling the Patroni API - PatroniAPICheckInterval Duration `json:"patroni_api_check_interval,omitempty"` + PatroniAPICheckInterval *metav1.Duration `json:"patroni_api_check_interval,omitempty"` // +kubebuilder:default="5s" // timeout when waiting for successful response from Patroni API - PatroniAPICheckTimeout Duration `json:"patroni_api_check_timeout,omitempty"` + PatroniAPICheckTimeout *metav1.Duration `json:"patroni_api_check_timeout,omitempty"` } // LoadBalancerConfiguration defines the LB configuration @@ -420,10 +418,10 @@ type OperatorConfigurationData struct { Workers uint32 `json:"workers,omitempty"` // +kubebuilder:default="30m" // period between consecutive sync requests - ResyncPeriod Duration `json:"resync_period,omitempty"` + ResyncPeriod *metav1.Duration `json:"resync_period,omitempty"` // +kubebuilder:default="5m" // period between consecutive repair requests - RepairPeriod Duration `json:"repair_period,omitempty"` + RepairPeriod *metav1.Duration `json:"repair_period,omitempty"` // +kubebuilder:default=true EnableMaintenanceWindows *bool `json:"enable_maintenance_windows,omitempty"` // +kubebuilder:validation:Type=array @@ -478,6 +476,3 @@ type OperatorConfigurationData struct { IgnoreInstanceLimitsAnnotationKey string `json:"ignore_instance_limits_annotation_key,omitempty"` IgnoreResourcesLimitsAnnotationKey string `json:"ignore_resources_limits_annotation_key,omitempty"` } - -// Duration shortens this frequently used name -type Duration time.Duration diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 19d957431..9cd6b27b7 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -447,8 +447,7 @@ spec: default: 20m description: timeout for successful migration of master pods from unschedulable node - format: int64 - type: integer + type: string node_readiness_label: additionalProperties: type: string @@ -521,8 +520,7 @@ spec: default: 5m description: Postgres pods are terminated forcefully after this timeout - format: int64 - type: integer + type: string secret_name_template: default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' description: |- @@ -783,13 +781,11 @@ spec: repair_period: default: 5m description: period between consecutive repair requests - format: int64 - type: integer + type: string resync_period: default: 30m description: period between consecutive sync requests - format: int64 - type: integer + type: string scalyr: description: ScalyrConfiguration defines the configuration for ScalyrAPI properties: @@ -885,48 +881,40 @@ spec: default: 1s description: interval between consecutive attempts of operator calling the Patroni API - format: int64 - type: integer + type: string patroni_api_check_timeout: default: 5s description: timeout when waiting for successful response from Patroni API - format: int64 - type: integer + type: string pod_deletion_wait_timeout: default: 10m description: timeout when waiting for the Postgres pods to be deleted - format: int64 - type: integer + type: string pod_label_wait_timeout: default: 10m description: timeout when waiting for pod role and cluster labels - format: int64 - type: integer + type: string ready_wait_interval: default: 4s description: interval between consecutive attempts waiting for postgresql CRD to be created - format: int64 - type: integer + type: string ready_wait_timeout: default: 30s description: timeout for the complete postgres CRD creation - format: int64 - type: integer + type: string resource_check_interval: default: 3s description: interval to wait between consecutive attempts to check for some K8s resources - format: int64 - type: integer + type: string resource_check_timeout: default: 10m description: timeout when waiting for the presence of a certain K8s resource - format: int64 - type: integer + type: string type: object users: description: PostgresUsersConfiguration defines the system users of 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 4fd9c4a00..ddccccf7f 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -163,6 +163,11 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura *out = new(bool) **out = **in } + if in.PodTerminateGracePeriod != nil { + in, out := &in.PodTerminateGracePeriod, &out.PodTerminateGracePeriod + *out = new(metav1.Duration) + **out = **in + } if in.LivenessProbe != nil { in, out := &in.LivenessProbe, &out.LivenessProbe *out = new(corev1.Probe) @@ -280,6 +285,11 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura } } out.PodEnvironmentConfigMap = in.PodEnvironmentConfigMap + if in.MasterPodMoveTimeout != nil { + in, out := &in.MasterPodMoveTimeout, &out.MasterPodMoveTimeout + *out = new(metav1.Duration) + **out = **in + } if in.PersistentVolumeClaimRetentionPolicy != nil { in, out := &in.PersistentVolumeClaimRetentionPolicy, &out.PersistentVolumeClaimRetentionPolicy *out = make(map[string]string, len(*in)) @@ -433,6 +443,16 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData *out = make([]string, len(*in)) copy(*out, *in) } + if in.ResyncPeriod != nil { + in, out := &in.ResyncPeriod, &out.ResyncPeriod + *out = new(metav1.Duration) + **out = **in + } + if in.RepairPeriod != nil { + in, out := &in.RepairPeriod, &out.RepairPeriod + *out = new(metav1.Duration) + **out = **in + } if in.EnableMaintenanceWindows != nil { in, out := &in.EnableMaintenanceWindows, &out.EnableMaintenanceWindows *out = new(bool) @@ -466,7 +486,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData in.MajorVersionUpgrade.DeepCopyInto(&out.MajorVersionUpgrade) in.Kubernetes.DeepCopyInto(&out.Kubernetes) out.PostgresPodResources = in.PostgresPodResources - out.Timeouts = in.Timeouts + in.Timeouts.DeepCopyInto(&out.Timeouts) in.LoadBalancer.DeepCopyInto(&out.LoadBalancer) out.AWSGCP = in.AWSGCP in.OperatorDebug.DeepCopyInto(&out.OperatorDebug) @@ -582,6 +602,46 @@ func (in *OperatorLogicalBackupConfiguration) DeepCopy() *OperatorLogicalBackupC // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperatorTimeouts) DeepCopyInto(out *OperatorTimeouts) { *out = *in + if in.ResourceCheckInterval != nil { + in, out := &in.ResourceCheckInterval, &out.ResourceCheckInterval + *out = new(metav1.Duration) + **out = **in + } + if in.ResourceCheckTimeout != nil { + in, out := &in.ResourceCheckTimeout, &out.ResourceCheckTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.PodLabelWaitTimeout != nil { + in, out := &in.PodLabelWaitTimeout, &out.PodLabelWaitTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.PodDeletionWaitTimeout != nil { + in, out := &in.PodDeletionWaitTimeout, &out.PodDeletionWaitTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.ReadyWaitInterval != nil { + in, out := &in.ReadyWaitInterval, &out.ReadyWaitInterval + *out = new(metav1.Duration) + **out = **in + } + if in.ReadyWaitTimeout != nil { + in, out := &in.ReadyWaitTimeout, &out.ReadyWaitTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.PatroniAPICheckInterval != nil { + in, out := &in.PatroniAPICheckInterval, &out.PatroniAPICheckInterval + *out = new(metav1.Duration) + **out = **in + } + if in.PatroniAPICheckTimeout != nil { + in, out := &in.PatroniAPICheckTimeout, &out.PatroniAPICheckTimeout + *out = new(metav1.Duration) + **out = **in + } return } diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 95a445ff3..00e69d4d7 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -138,7 +138,8 @@ func TestCreate(t *testing.T) { var cluster = New( Config{ OpConfig: config.Config{ - PodManagementPolicy: "ordered_ready", + PodManagementPolicy: "ordered_ready", + PodTerminateGracePeriod: &metav1.Duration{Duration: 600 * time.Second}, Resources: config.Resources{ ClusterLabels: map[string]string{"application": "spilo"}, ClusterNameLabel: "cluster-name", @@ -147,8 +148,8 @@ func TestCreate(t *testing.T) { DefaultMemoryRequest: "300Mi", DefaultMemoryLimit: "300Mi", PodRoleLabel: "spilo-role", - ResourceCheckInterval: time.Duration(3), - ResourceCheckTimeout: time.Duration(10), + ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second}, + ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute}, }, EnableFinalizers: util.True(), }, diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 85685774d..61cd9b041 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -292,7 +292,7 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( if connectionPoolerSpec == nil { connectionPoolerSpec = &acidv1.ConnectionPooler{} } - gracePeriod := int64(c.OpConfig.PodTerminateGracePeriod.Seconds()) + gracePeriod := int64(util.CoalesceDuration(c.OpConfig.PodTerminateGracePeriod, "5m").Seconds()) resources, err := c.generateResourceRequirements( connectionPoolerSpec.Resources, makeDefaultConnectionPoolerResources(&c.OpConfig), diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 306185e90..2b05e2c4a 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -1165,7 +1165,7 @@ func (c *Cluster) getPodEnvironmentSecretVariables() ([]v1.EnvVar, error) { secret := &v1.Secret{} var notFoundErr error - err := retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, + err := retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration, func() (bool, error) { var err error secret, err = c.KubeClient.Secrets(c.Namespace).Get( @@ -1498,7 +1498,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef &tolerationSpec, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity), spec.SchedulerName, - int64(c.OpConfig.PodTerminateGracePeriod.Seconds()), + int64(util.CoalesceDuration(c.OpConfig.PodTerminateGracePeriod, "5m").Seconds()), c.OpConfig.PodServiceAccountName, c.OpConfig.KubeIAMRole, effectivePodPriorityClassName, @@ -2431,7 +2431,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { &tolerationsSpec, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil), nil, - int64(c.OpConfig.PodTerminateGracePeriod.Seconds()), + int64(util.CoalesceDuration(c.OpConfig.PodTerminateGracePeriod, "5m").Seconds()), c.OpConfig.PodServiceAccountName, c.OpConfig.KubeIAMRole, "", diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 8cbecb2fa..3f385b456 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -6,7 +6,6 @@ import ( "reflect" "sort" "testing" - "time" "github.com/stretchr/testify/assert" @@ -361,8 +360,8 @@ func TestPodEnvironmentSecretVariables(t *testing.T) { opConfig: config.Config{ Resources: config.Resources{ PodEnvironmentSecret: testPodEnvironmentObjectNotExists, - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, }, }, err: fmt.Errorf("could not read Secret PodEnvironmentSecretName: still failing after %d retries: secret.core %q not found", maxRetries, testPodEnvironmentObjectNotExists), @@ -372,8 +371,8 @@ func TestPodEnvironmentSecretVariables(t *testing.T) { opConfig: config.Config{ Resources: config.Resources{ PodEnvironmentSecret: testPodEnvironmentSecretNameAPIError, - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, }, }, err: fmt.Errorf("could not read Secret PodEnvironmentSecretName: Secret PodEnvironmentSecret API error"), @@ -383,8 +382,8 @@ func TestPodEnvironmentSecretVariables(t *testing.T) { opConfig: config.Config{ Resources: config.Resources{ PodEnvironmentSecret: testPodEnvironmentSecretName, - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, }, }, envVars: []v1.EnvVar{ @@ -848,8 +847,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { Name: testPodEnvironmentConfigMapName, }, PodEnvironmentSecret: testPodEnvironmentSecretName, - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, }, }, cloneDescription: &acidv1.CloneDescription{}, @@ -877,8 +876,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { Name: testPodEnvironmentConfigMapName, }, PodEnvironmentSecret: testPodEnvironmentSecretName, - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, }, }, cloneDescription: &acidv1.CloneDescription{}, @@ -968,8 +967,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { opConfig: config.Config{ Resources: config.Resources{ PodEnvironmentSecret: testPodEnvironmentSecretName, - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, }, WALES3Bucket: "global-s3-bucket", }, @@ -986,8 +985,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { opConfig: config.Config{ Resources: config.Resources{ PodEnvironmentSecret: testPodEnvironmentSecretName, - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, }, WALES3Bucket: "global-s3-bucket", }, diff --git a/pkg/cluster/pod.go b/pkg/cluster/pod.go index 6658ba414..c18054aad 100644 --- a/pkg/cluster/pod.go +++ b/pkg/cluster/pod.go @@ -334,7 +334,7 @@ func (c *Cluster) getPatroniConfig(pod *v1.Pod) (acidv1.Patroni, map[string]stri pgParameters map[string]string ) podName := util.NameFromMeta(pod.ObjectMeta) - err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout, + err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval.Duration, c.OpConfig.PatroniAPICheckTimeout.Duration, func() (bool, error) { var err error patroniConfig, pgParameters, err = c.patroni.GetConfig(pod) @@ -355,7 +355,7 @@ func (c *Cluster) getPatroniConfig(pod *v1.Pod) (acidv1.Patroni, map[string]stri func (c *Cluster) getPatroniMemberData(pod *v1.Pod) (patroni.MemberData, error) { var memberData patroni.MemberData - err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout, + err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval.Duration, c.OpConfig.PatroniAPICheckTimeout.Duration, func() (bool, error) { var err error memberData, err = c.patroni.GetMemberData(pod) @@ -506,7 +506,7 @@ func (c *Cluster) getSwitchoverCandidate(master *v1.Pod) (spec.NamespacedName, e candidates := make([]patroni.ClusterMember, 0) syncCandidates := make([]patroni.ClusterMember, 0) - err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout, + err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval.Duration, c.OpConfig.PatroniAPICheckTimeout.Duration, func() (bool, error) { var err error members, err = c.patroni.GetClusterMembers(master) diff --git a/pkg/cluster/pod_test.go b/pkg/cluster/pod_test.go index 6ab3f9207..0eb1791e2 100644 --- a/pkg/cluster/pod_test.go +++ b/pkg/cluster/pod_test.go @@ -16,6 +16,7 @@ import ( "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" ) func TestGetSwitchoverCandidate(t *testing.T) { @@ -28,8 +29,8 @@ func TestGetSwitchoverCandidate(t *testing.T) { var cluster = New( Config{ OpConfig: config.Config{ - PatroniAPICheckInterval: time.Duration(1), - PatroniAPICheckTimeout: time.Duration(5), + PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second}, + PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second}, }, }, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder) diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index 1fdad3e5e..6053de471 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -234,7 +234,7 @@ func (c *Cluster) replaceStatefulSet(newStatefulSet *appsv1.StatefulSet) error { // wait until the statefulset is truly deleted c.logger.Debug("waiting for the statefulset to be deleted") - err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, + err = retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration, func() (bool, error) { _, err2 := c.KubeClient.StatefulSets(oldStatefulset.Namespace).Get(context.TODO(), oldStatefulset.Name, metav1.GetOptions{}) if err2 == nil { @@ -550,7 +550,7 @@ func (c *Cluster) deletePrimaryPodDisruptionBudget() error { c.logger.Infof("pod disruption budget %q has been deleted", util.NameFromMeta(c.PrimaryPodDisruptionBudget.ObjectMeta)) c.PrimaryPodDisruptionBudget = nil - err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, + err = retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration, func() (bool, error) { _, err2 := c.KubeClient.PodDisruptionBudgets(pdbName.Namespace).Get(context.TODO(), pdbName.Name, metav1.GetOptions{}) if err2 == nil { @@ -588,7 +588,7 @@ func (c *Cluster) deleteCriticalOpPodDisruptionBudget() error { c.logger.Infof("pod disruption budget %q has been deleted", util.NameFromMeta(c.CriticalOpPodDisruptionBudget.ObjectMeta)) c.CriticalOpPodDisruptionBudget = nil - err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, + err = retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration, func() (bool, error) { _, err2 := c.KubeClient.PodDisruptionBudgets(pdbName.Namespace).Get(context.TODO(), pdbName.Name, metav1.GetOptions{}) if err2 == nil { diff --git a/pkg/cluster/sync_test.go b/pkg/cluster/sync_test.go index f7d46d427..1d21d1536 100644 --- a/pkg/cluster/sync_test.go +++ b/pkg/cluster/sync_test.go @@ -101,8 +101,8 @@ func TestSyncStatefulSetsAnnotations(t *testing.T) { DefaultMemoryLimit: "300Mi", InheritedAnnotations: []string{inheritedAnnotation}, PodRoleLabel: "spilo-role", - ResourceCheckInterval: time.Duration(3), - ResourceCheckTimeout: time.Duration(10), + ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second}, + ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute}, }, }, }, client, pg, logger, eventRecorder) @@ -187,8 +187,8 @@ func TestPodAnnotationsSync(t *testing.T) { var cluster = New( Config{ OpConfig: config.Config{ - PatroniAPICheckInterval: time.Duration(1), - PatroniAPICheckTimeout: time.Duration(5), + PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second}, + PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second}, PodManagementPolicy: "ordered_ready", CustomPodAnnotations: customPodAnnotations, ConnectionPooler: config.ConnectionPooler{ @@ -207,8 +207,8 @@ func TestPodAnnotationsSync(t *testing.T) { DefaultMemoryLimit: "300Mi", MaxInstances: -1, PodRoleLabel: "spilo-role", - ResourceCheckInterval: time.Duration(3), - ResourceCheckTimeout: time.Duration(10), + ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second}, + ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute}, }, }, }, client, pg, logger, eventRecorder) @@ -381,8 +381,8 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { DefaultMemoryRequest: "300Mi", DefaultMemoryLimit: "300Mi", PodRoleLabel: "spilo-role", - ResourceCheckInterval: time.Duration(3), - ResourceCheckTimeout: time.Duration(10), + ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second}, + ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute}, }, }, }, client, pg, logger, eventRecorder) @@ -694,8 +694,8 @@ func TestSyncStandbyClusterConfiguration(t *testing.T) { var cluster = New( Config{ OpConfig: config.Config{ - PatroniAPICheckInterval: time.Duration(1), - PatroniAPICheckTimeout: time.Duration(5), + PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second}, + PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second}, PodManagementPolicy: "ordered_ready", Resources: config.Resources{ ClusterLabels: map[string]string{"application": applicationLabel}, @@ -707,8 +707,8 @@ func TestSyncStandbyClusterConfiguration(t *testing.T) { MinInstances: int32(-1), MaxInstances: int32(-1), PodRoleLabel: "spilo-role", - ResourceCheckInterval: time.Duration(3), - ResourceCheckTimeout: time.Duration(10), + ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second}, + ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute}, }, }, }, client, pg, logger, eventRecorder) diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index cbcccd16e..c3a9dda31 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -342,7 +342,7 @@ func (c *Cluster) annotationsSet(annotations map[string]string) map[string]strin } func (c *Cluster) waitForPodLabel(podEvents chan PodEvent, stopCh chan struct{}, role *PostgresRole) (*v1.Pod, error) { - timeout := time.After(c.OpConfig.PodLabelWaitTimeout) + timeout := time.After(c.OpConfig.PodLabelWaitTimeout.Duration) for { select { case podEvent := <-podEvents: @@ -364,7 +364,7 @@ func (c *Cluster) waitForPodLabel(podEvents chan PodEvent, stopCh chan struct{}, } func (c *Cluster) waitForPodDeletion(podEvents chan PodEvent) error { - timeout := time.After(c.OpConfig.PodDeletionWaitTimeout) + timeout := time.After(c.OpConfig.PodDeletionWaitTimeout.Duration) for { select { case podEvent := <-podEvents: @@ -378,7 +378,7 @@ func (c *Cluster) waitForPodDeletion(podEvents chan PodEvent) error { } func (c *Cluster) waitStatefulsetReady() error { - return retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, + return retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration, func() (bool, error) { listOptions := metav1.ListOptions{ LabelSelector: c.labelsSet(false).String(), @@ -428,7 +428,7 @@ func (c *Cluster) _waitPodLabelsReady(anyReplica bool) error { c.logger.Debug("Waiting for any replica pod to become ready") } - err := retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, + err := retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration, func() (bool, error) { masterCount := 0 if !anyReplica { diff --git a/pkg/cluster/util_test.go b/pkg/cluster/util_test.go index 8413ca396..0239abf45 100644 --- a/pkg/cluster/util_test.go +++ b/pkg/cluster/util_test.go @@ -297,8 +297,8 @@ func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster, cluster := New( Config{ OpConfig: config.Config{ - PatroniAPICheckInterval: time.Duration(1), - PatroniAPICheckTimeout: time.Duration(5), + PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second}, + PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second}, KubernetesUseConfigMaps: true, ConnectionPooler: config.ConnectionPooler{ ConnectionPoolerDefaultCPURequest: "100m", @@ -318,8 +318,8 @@ func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster, DefaultMemoryLimit: "300Mi", InheritedAnnotations: []string{"owned-by"}, PodRoleLabel: "spilo-role", - ResourceCheckInterval: time.Duration(testResourceCheckInterval), - ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval}, + ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout}, MinInstances: -1, MaxInstances: -1, }, diff --git a/pkg/controller/node.go b/pkg/controller/node.go index 730a9133b..978fda130 100644 --- a/pkg/controller/node.go +++ b/pkg/controller/node.go @@ -152,7 +152,7 @@ func (c *Controller) nodeDelete(obj interface{}) { func (c *Controller) moveMasterPodsOffNode(node *v1.Node) { // retry to move master until configured timeout is reached - err := retryutil.Retry(1*time.Minute, c.opConfig.MasterPodMoveTimeout, + err := retryutil.Retry(1*time.Minute, c.opConfig.MasterPodMoveTimeout.Duration, func() (bool, error) { err := c.attemptToMoveMasterPodsOffNode(node) if err != nil { diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 94ef86d7c..55f1f00d8 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -4,8 +4,6 @@ import ( "context" "fmt" - "time" - acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" "github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util/config" @@ -44,8 +42,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.MaxInstances = fromCRD.MaxInstances result.IgnoreInstanceLimitsAnnotationKey = fromCRD.IgnoreInstanceLimitsAnnotationKey result.IgnoreResourcesLimitsAnnotationKey = fromCRD.IgnoreResourcesLimitsAnnotationKey - result.ResyncPeriod = util.CoalesceDuration(time.Duration(fromCRD.ResyncPeriod), "30m") - result.RepairPeriod = util.CoalesceDuration(time.Duration(fromCRD.RepairPeriod), "5m") + result.ResyncPeriod = util.CoalesceDuration(fromCRD.ResyncPeriod, "30m") + result.RepairPeriod = util.CoalesceDuration(fromCRD.RepairPeriod, "5m") result.SetMemoryRequestToLimit = fromCRD.SetMemoryRequestToLimit result.ShmVolume = util.CoalesceBool(fromCRD.ShmVolume, util.True()) result.SidecarImages = fromCRD.SidecarImages @@ -75,7 +73,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap result.PodEnvironmentSecret = fromCRD.Kubernetes.PodEnvironmentSecret - result.PodTerminateGracePeriod = util.CoalesceDuration(time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod), "5m") + result.PodTerminateGracePeriod = util.CoalesceDuration(fromCRD.Kubernetes.PodTerminateGracePeriod, "5m") result.LivenessProbe = fromCRD.Kubernetes.LivenessProbe result.SpiloPrivileged = fromCRD.Kubernetes.SpiloPrivileged result.SpiloAllowPrivilegeEscalation = util.CoalesceBool(fromCRD.Kubernetes.SpiloAllowPrivilegeEscalation, util.True()) @@ -129,7 +127,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.EnableSecretsDeletion = util.CoalesceBool(fromCRD.Kubernetes.EnableSecretsDeletion, util.True()) result.EnablePersistentVolumeClaimDeletion = util.CoalesceBool(fromCRD.Kubernetes.EnablePersistentVolumeClaimDeletion, util.True()) result.EnableReadinessProbe = fromCRD.Kubernetes.EnableReadinessProbe - result.MasterPodMoveTimeout = util.CoalesceDuration(time.Duration(fromCRD.Kubernetes.MasterPodMoveTimeout), "10m") + result.MasterPodMoveTimeout = util.CoalesceDuration(fromCRD.Kubernetes.MasterPodMoveTimeout, "10m") result.EnablePodAntiAffinity = fromCRD.Kubernetes.EnablePodAntiAffinity result.PodAntiAffinityTopologyKey = util.Coalesce(fromCRD.Kubernetes.PodAntiAffinityTopologyKey, "kubernetes.io/hostname") result.PodAntiAffinityPreferredDuringScheduling = fromCRD.Kubernetes.PodAntiAffinityPreferredDuringScheduling @@ -146,14 +144,14 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.MaxMemoryRequest = fromCRD.PostgresPodResources.MaxMemoryRequest // timeout config - result.ResourceCheckInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ResourceCheckInterval), "3s") - result.ResourceCheckTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ResourceCheckTimeout), "10m") - result.PodLabelWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PodLabelWaitTimeout), "10m") - result.PodDeletionWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PodDeletionWaitTimeout), "10m") - result.ReadyWaitInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ReadyWaitInterval), "4s") - result.ReadyWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ReadyWaitTimeout), "30s") - result.PatroniAPICheckInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PatroniAPICheckInterval), "1s") - result.PatroniAPICheckTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PatroniAPICheckTimeout), "5s") + result.ResourceCheckInterval = util.CoalesceDuration(fromCRD.Timeouts.ResourceCheckInterval, "3s") + result.ResourceCheckTimeout = util.CoalesceDuration(fromCRD.Timeouts.ResourceCheckTimeout, "10m") + result.PodLabelWaitTimeout = util.CoalesceDuration(fromCRD.Timeouts.PodLabelWaitTimeout, "10m") + result.PodDeletionWaitTimeout = util.CoalesceDuration(fromCRD.Timeouts.PodDeletionWaitTimeout, "10m") + result.ReadyWaitInterval = util.CoalesceDuration(fromCRD.Timeouts.ReadyWaitInterval, "4s") + result.ReadyWaitTimeout = util.CoalesceDuration(fromCRD.Timeouts.ReadyWaitTimeout, "30s") + result.PatroniAPICheckInterval = util.CoalesceDuration(fromCRD.Timeouts.PatroniAPICheckInterval, "1s") + result.PatroniAPICheckTimeout = util.CoalesceDuration(fromCRD.Timeouts.PatroniAPICheckTimeout, "5s") // load balancer config result.DbHostedZone = util.Coalesce(fromCRD.LoadBalancer.DbHostedZone, "db.example.com") diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index 0933f7823..942ff9951 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -27,7 +27,7 @@ import ( func (c *Controller) clusterResync(stopCh <-chan struct{}, wg *sync.WaitGroup) { defer wg.Done() - ticker := time.NewTicker(c.opConfig.ResyncPeriod) + ticker := time.NewTicker(c.opConfig.ResyncPeriod.Duration) for { select { diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 43fa37a33..85119b18b 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -3,33 +3,33 @@ package config import ( "encoding/json" "strings" - "time" "fmt" "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util/constants" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // CRD describes CustomResourceDefinition specific configuration parameters type CRD struct { - ReadyWaitInterval time.Duration `name:"ready_wait_interval" default:"4s"` - ReadyWaitTimeout time.Duration `name:"ready_wait_timeout" default:"30s"` - ResyncPeriod time.Duration `name:"resync_period" default:"30m"` - RepairPeriod time.Duration `name:"repair_period" default:"5m"` - EnableCRDRegistration *bool `name:"enable_crd_registration" default:"true"` - CRDCategories []string `name:"crd_categories" default:"all"` + ReadyWaitInterval *metav1.Duration `name:"ready_wait_interval" default:"4s"` + ReadyWaitTimeout *metav1.Duration `name:"ready_wait_timeout" default:"30s"` + ResyncPeriod *metav1.Duration `name:"resync_period" default:"30m"` + RepairPeriod *metav1.Duration `name:"repair_period" default:"5m"` + EnableCRDRegistration *bool `name:"enable_crd_registration" default:"true"` + CRDCategories []string `name:"crd_categories" default:"all"` } // 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"` - PodDeletionWaitTimeout time.Duration `name:"pod_deletion_wait_timeout" default:"10m"` - PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"` + ResourceCheckInterval *metav1.Duration `name:"resource_check_interval" default:"3s"` + ResourceCheckTimeout *metav1.Duration `name:"resource_check_timeout" default:"10m"` + PodLabelWaitTimeout *metav1.Duration `name:"pod_label_wait_timeout" default:"10m"` + PodDeletionWaitTimeout *metav1.Duration `name:"pod_deletion_wait_timeout" default:"10m"` + PodTerminateGracePeriod *metav1.Duration `name:"pod_terminate_grace_period" default:"5m"` LivenessProbe *v1.Probe `name:"-"` SpiloRunAsUser *int64 `name:"spilo_runasuser"` SpiloRunAsGroup *int64 `name:"spilo_runasgroup"` @@ -188,7 +188,7 @@ type Config struct { // value of this string must be valid JSON or YAML; see initPodServiceAccount PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""` PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""` - MasterPodMoveTimeout time.Duration `name:"master_pod_move_timeout" default:"20m"` + MasterPodMoveTimeout *metav1.Duration `name:"master_pod_move_timeout" default:"20m"` DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"` AWSRegion string `name:"aws_region" default:"eu-central-1"` WALES3Bucket string `name:"wal_s3_bucket"` @@ -242,7 +242,7 @@ type Config struct { RingLogLines int `name:"ring_log_lines" default:"100"` ClusterHistoryEntries int `name:"cluster_history_entries" default:"1000"` TeamAPIRoleConfiguration map[string]string `name:"team_api_role_configuration" default:"log_statement:all"` - PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"` + PodTerminateGracePeriod *metav1.Duration `name:"pod_terminate_grace_period" default:"5m"` PodManagementPolicy string `name:"pod_management_policy" default:"ordered_ready"` EnableReadinessProbe bool `name:"enable_readiness_probe" default:"false"` ProtectedRoles []string `name:"protected_role_names" default:"admin,cron_admin"` @@ -258,8 +258,8 @@ type Config struct { MajorVersionUpgradeTeamAllowList []string `name:"major_version_upgrade_team_allow_list" default:""` MinimalMajorVersion string `name:"minimal_major_version" default:"14"` TargetMajorVersion string `name:"target_major_version" default:"18"` - PatroniAPICheckInterval time.Duration `name:"patroni_api_check_interval" default:"1s"` - PatroniAPICheckTimeout time.Duration `name:"patroni_api_check_timeout" default:"5s"` + PatroniAPICheckInterval *metav1.Duration `name:"patroni_api_check_interval" default:"1s"` + PatroniAPICheckTimeout *metav1.Duration `name:"patroni_api_check_timeout" default:"5s"` EnablePatroniFailsafeMode *bool `name:"enable_patroni_failsafe_mode" default:"false"` EnableSecretsDeletion *bool `name:"enable_secrets_deletion" default:"true"` EnablePersistentVolumeClaimDeletion *bool `name:"enable_persistent_volume_claim_deletion" default:"true"` diff --git a/pkg/util/config/config_test.go b/pkg/util/config/config_test.go index c5fd48d93..6373fdc9a 100644 --- a/pkg/util/config/config_test.go +++ b/pkg/util/config/config_test.go @@ -230,16 +230,16 @@ var newFromMapTests = []struct { { description: "duration parsing", input: map[string]string{ - "ready_wait_interval": "10s", - "ready_wait_timeout": "1m", + "patroni_api_check_interval": "1s", + "patroni_api_check_timeout": "5s", }, expectPanic: false, validateFunc: func(t *testing.T, cfg *Config) { - if cfg.ReadyWaitInterval.Seconds() != 10 { - t.Errorf("expected ReadyWaitInterval=10s, got %v", cfg.ReadyWaitInterval) + if cfg.PatroniAPICheckInterval.Seconds() != 1 { + t.Errorf("expected check interval of 1s, got %.0fs", cfg.PatroniAPICheckInterval.Seconds()) } - if cfg.ReadyWaitTimeout.Minutes() != 1 { - t.Errorf("expected ReadyWaitTimeout=1m, got %v", cfg.ReadyWaitTimeout) + if cfg.PatroniAPICheckTimeout.Seconds() != 5 { + t.Errorf("expected check timeout of 5s, got %.0fs", cfg.PatroniAPICheckTimeout.Seconds()) } }, }, diff --git a/pkg/util/config/util.go b/pkg/util/config/util.go index 4c1bdf7e0..ae1deb461 100644 --- a/pkg/util/config/util.go +++ b/pkg/util/config/util.go @@ -7,6 +7,8 @@ import ( "strconv" "strings" "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type decoder interface { @@ -101,13 +103,7 @@ func processField(value string, field reflect.Value) error { val int64 err error ) - if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" { - var d time.Duration - d, err = time.ParseDuration(value) - val = int64(d) - } else { - val, err = strconv.ParseInt(value, 0, typ.Bits()) - } + val, err = strconv.ParseInt(value, 0, typ.Bits()) if err != nil { return err } @@ -165,6 +161,15 @@ func processField(value string, field reflect.Value) error { mp.SetMapIndex(k, v) } field.Set(mp) + case reflect.Struct: + if typ.Name() == "Duration" { + var d time.Duration + d, err := time.ParseDuration(value) + if err != nil { + return err + } + field.Set(reflect.ValueOf(metav1.Duration{Duration: d})) + } } return nil diff --git a/pkg/util/util.go b/pkg/util/util.go index 993a773c4..79ff52282 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -311,14 +311,14 @@ func CoalesceBool(val, defaultVal *bool) *bool { return val } -// CoalesceDuration works like coalesce but for time.Duration -func CoalesceDuration(val time.Duration, defaultVal string) time.Duration { - if val == 0 { +// CoalesceDuration works like coalesce but for metav1.Duration +func CoalesceDuration(val *metav1.Duration, defaultVal string) *metav1.Duration { + if val == nil || val.Duration == 0 { duration, err := time.ParseDuration(defaultVal) if err != nil { panic(err) } - return duration + return &metav1.Duration{Duration: duration} } return val } From a3d17c12d27ba63b1497fc993ef566759b828cf9 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Thu, 2 Jul 2026 14:04:57 +0200 Subject: [PATCH 40/70] document changed configmap default (#3116) * document changed configmap default * update config reference * add warning message in sync * address review comments --- charts/postgres-operator/values.yaml | 2 +- docs/migrate.md | 17 +++++++ docs/reference/operator_parameters.md | 6 +-- ...gresql-operator-default-configuration.yaml | 2 +- .../v1/operator_configuration_type.go | 2 +- .../acid.zalan.do/v1/zz_generated.deepcopy.go | 5 ++ pkg/cluster/cluster_test.go | 1 + pkg/cluster/k8sres_test.go | 49 ++++++++++--------- pkg/cluster/sync.go | 4 ++ pkg/cluster/util.go | 5 +- pkg/cluster/util_test.go | 6 +-- pkg/controller/operator_config.go | 2 +- pkg/util/config/config.go | 2 +- 13 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 docs/migrate.md diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index b880916f5..3a2194ef0 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -50,7 +50,7 @@ configGeneral: # ignore_resources_limits_annotation_key: "" # Select if setup uses endpoints (default), or configmaps to manage leader (DCS=k8s) - # kubernetes_use_configmaps: false + kubernetes_use_configmaps: true # maintenance windows applied to all Postgres clusters unless overridden in the manifest # maintenance_windows: diff --git a/docs/migrate.md b/docs/migrate.md new file mode 100644 index 000000000..7ec5befb8 --- /dev/null +++ b/docs/migrate.md @@ -0,0 +1,17 @@ +

Migrate from v1 to v2

+ +Version 2.0 changes some default settings and removes deprecated fields. Please read the following sections before upgrading the Postgres Operator deployment. + +## K8s Endpoints are deprecated + +If your current operator v1.x deployment is relying on K8s endpoints (the default setup) for Patroni to manage the HA state you have to start planning to switch to configmaps, because endpoints are deprecated from K8s 1.33 onwards. The default of the corresponding parameter `kubernetes_use_configmaps` is changing to `true` with v2.0 of the operator. This means you have to explicity set it to `false` in your configuration before you start the upgrade. + +We explicitly warn you to go straight to configmap-based HA management with database clusters that use replicas, because there's is a danger to run into split-brain scenarios during the rolling update of pods. To play it safe, here is what you should do - before or after the Postgres Operator upgrade: + +1. Scale-in all your database clusters to only one primary instance. This can be done by changing the global config options `max_instances` and `min_instances` to `1`. If you have allowed users to ignore globally defined instance limits by configuring an `ignore_instance_limits_annotation_key`, remove it for now. + +2. Wait for all clusters to be healthy and change the `kubernetes_use_configmaps` setting to `true`. This will trigger the replacement of the primary pod of all clusters and cause downtime for as long as the pods are rescheduled and start up. + +3. Check again that all clusters are healthy with configmaps created. There should be three for each cluster called like cluster name with suffixes `-config`, `-failover` and `-leader`. Now, revert the changes from step 1 and scale-out the to number of instances set in the manifests. + +4. The orphaned endpoints, which use the same names like the new configmaps, have to be deleted by you or your K8s garbage collection. diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 13d29c950..dd7487352 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -100,15 +100,13 @@ Those are top-level keys, containing both leaf keys and groups. Kubernetes-native DCS). * **kubernetes_use_configmaps** - Select if setup uses endpoints (default), or configmaps to manage leader when + Select if setup uses endpoints or configmaps (default) to manage leader when DCS is kubernetes (not etcd or similar). In OpenShift it is not possible to use endpoints option, and configmaps is required. Starting with K8s 1.33, endpoints are marked as deprecated. It's recommended to switch to config maps instead. But, to do so make sure you scale the Postgres cluster down to just one primary pod (e.g. using `max_instances` option). Otherwise, you risk - running into a split-brain scenario. - By default, `kubernetes_use_configmaps: false`, meaning endpoints will be used. - Starting from v1.16.0 the default will be changed to `true`. + running into a split-brain scenario. Default is `true`. * **docker_image** Spilo Docker image for Postgres instances. For production, don't rely on the diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 88af48b66..daa7f7b13 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -16,7 +16,7 @@ configuration: etcd_host: "" # ignore_instance_limits_annotation_key: "" # ignore_resources_limits_annotation_key: "" - # kubernetes_use_configmaps: false + kubernetes_use_configmaps: true # maintenance_windows: # - "Sat:22:00-23:59" # - "Sun:00:00-01:00" 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 27063fd23..b84bce03a 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -410,7 +410,7 @@ type OperatorConfigurationData struct { // +kubebuilder:default="" EtcdHost string `json:"etcd_host,omitempty"` // +kubebuilder:default=true - KubernetesUseConfigMaps bool `json:"kubernetes_use_configmaps,omitempty"` + KubernetesUseConfigMaps *bool `json:"kubernetes_use_configmaps,omitempty"` // +kubebuilder:default="ghcr.io/zalando/spilo-18:4.1-p1" DockerImage string `json:"docker_image,omitempty"` // +kubebuilder:validation:Minimum=1 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 ddccccf7f..83c4763ac 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -443,6 +443,11 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData *out = make([]string, len(*in)) copy(*out, *in) } + if in.KubernetesUseConfigMaps != nil { + in, out := &in.KubernetesUseConfigMaps, &out.KubernetesUseConfigMaps + *out = new(bool) + **out = **in + } if in.ResyncPeriod != nil { in, out := &in.ResyncPeriod, &out.ResyncPeriod *out = new(metav1.Duration) diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 00e69d4d7..b2c6bee68 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -95,6 +95,7 @@ func TestCreate(t *testing.T) { client := k8sutil.KubernetesClient{ DeploymentsGetter: clientSet.AppsV1(), + ConfigMapsGetter: clientSet.CoreV1(), CronJobsGetter: clientSet.BatchV1(), EndpointsGetter: clientSet.CoreV1(), PersistentVolumeClaimsGetter: clientSet.CoreV1(), diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 3f385b456..d51efb085 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -582,60 +582,60 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { } expectedValuesS3Bucket := []ExpectedValue{ { - envIndex: 15, + envIndex: 16, envVarConstant: "WAL_S3_BUCKET", envVarValue: "global-s3-bucket", }, { - envIndex: 16, + envIndex: 17, envVarConstant: "WAL_BUCKET_SCOPE_SUFFIX", envVarValue: fmt.Sprintf("/%s", dummyUUID), }, { - envIndex: 17, + envIndex: 18, envVarConstant: "WAL_BUCKET_SCOPE_PREFIX", envVarValue: "", }, } expectedValuesGCPCreds := []ExpectedValue{ { - envIndex: 15, + envIndex: 16, envVarConstant: "WAL_GS_BUCKET", envVarValue: "global-gs-bucket", }, { - envIndex: 16, + envIndex: 17, envVarConstant: "WAL_BUCKET_SCOPE_SUFFIX", envVarValue: fmt.Sprintf("/%s", dummyUUID), }, { - envIndex: 17, + envIndex: 18, envVarConstant: "WAL_BUCKET_SCOPE_PREFIX", envVarValue: "", }, { - envIndex: 18, + envIndex: 19, envVarConstant: "GOOGLE_APPLICATION_CREDENTIALS", envVarValue: "some-path-to-credentials", }, } expectedS3BucketConfigMap := []ExpectedValue{ { - envIndex: 17, + envIndex: 18, envVarConstant: "wal_s3_bucket", envVarValue: "global-s3-bucket-configmap", }, } expectedCustomS3BucketSpec := []ExpectedValue{ { - envIndex: 15, + envIndex: 16, envVarConstant: "WAL_S3_BUCKET", envVarValue: "custom-s3-bucket", }, } expectedCustomVariableSecret := []ExpectedValue{ { - envIndex: 16, + envIndex: 17, envVarConstant: "custom_variable", envVarValueRef: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ @@ -649,72 +649,72 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { } expectedCustomVariableConfigMap := []ExpectedValue{ { - envIndex: 16, + envIndex: 17, envVarConstant: "custom_variable", envVarValue: "configmap-test", }, } expectedCustomVariableSpec := []ExpectedValue{ { - envIndex: 15, + envIndex: 16, envVarConstant: "CUSTOM_VARIABLE", envVarValue: "spec-env-test", }, } expectedCloneEnvSpec := []ExpectedValue{ { - envIndex: 16, + envIndex: 17, envVarConstant: "CLONE_WALE_S3_PREFIX", envVarValue: "s3://another-bucket", }, { - envIndex: 19, + envIndex: 20, envVarConstant: "CLONE_WAL_BUCKET_SCOPE_PREFIX", envVarValue: "", }, { - envIndex: 20, + envIndex: 21, envVarConstant: "CLONE_AWS_ENDPOINT", envVarValue: "s3.eu-central-1.amazonaws.com", }, } expectedCloneEnvSpecEnv := []ExpectedValue{ { - envIndex: 15, + envIndex: 16, envVarConstant: "CLONE_WAL_BUCKET_SCOPE_PREFIX", envVarValue: "test-cluster", }, { - envIndex: 17, + envIndex: 18, envVarConstant: "CLONE_WALE_S3_PREFIX", envVarValue: "s3://another-bucket", }, { - envIndex: 21, + envIndex: 22, envVarConstant: "CLONE_AWS_ENDPOINT", envVarValue: "s3.eu-central-1.amazonaws.com", }, } expectedCloneEnvConfigMap := []ExpectedValue{ { - envIndex: 16, + envIndex: 17, envVarConstant: "CLONE_WAL_S3_BUCKET", envVarValue: "global-s3-bucket", }, { - envIndex: 17, + envIndex: 18, envVarConstant: "CLONE_WAL_BUCKET_SCOPE_SUFFIX", envVarValue: fmt.Sprintf("/%s", dummyUUID), }, { - envIndex: 21, + envIndex: 22, envVarConstant: "clone_aws_endpoint", envVarValue: "s3.eu-west-1.amazonaws.com", }, } expectedCloneEnvSecret := []ExpectedValue{ { - envIndex: 21, + envIndex: 22, envVarConstant: "clone_aws_access_key_id", envVarValueRef: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ @@ -728,12 +728,12 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) { } expectedStandbyEnvSecret := []ExpectedValue{ { - envIndex: 15, + envIndex: 16, envVarConstant: "STANDBY_WALE_GS_PREFIX", envVarValue: "gs://some/path/", }, { - envIndex: 20, + envIndex: 21, envVarConstant: "standby_google_application_credentials", envVarValueRef: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ @@ -2976,6 +2976,7 @@ func getServices(serviceType v1.ServiceType, sourceRanges []string, extTrafficPo ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy), LoadBalancerSourceRanges: sourceRanges, Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}}, + Selector: map[string]string{"spilo-role": "master", "application": "spilo", "cluster-name": clusterName}, Type: serviceType, }, { diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index e15b5fedc..af3f786df 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -67,6 +67,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { } }() + if !c.patroniKubernetesUseConfigMaps() { + c.logger.Warning("K8s endpoints are deprecated. Please, enable kubernetes_use_configmaps. Requires scale-in to a single primary, see v1 -> v2 migration docs!") + } + if err = c.syncFinalizer(); err != nil { c.logger.Debugf("could not sync finalizers: %v", err) } diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index c3a9dda31..c2a847ba4 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -627,9 +627,12 @@ func (c *Cluster) patroniKubernetesUseConfigMaps() bool { if !c.patroniUsesKubernetes() { return false } + if c.OpConfig.KubernetesUseConfigMaps == nil { + return true + } // otherwise, follow the operator configuration - return c.OpConfig.KubernetesUseConfigMaps + return *c.OpConfig.KubernetesUseConfigMaps } // Earlier arguments take priority diff --git a/pkg/cluster/util_test.go b/pkg/cluster/util_test.go index 0239abf45..d97668ee8 100644 --- a/pkg/cluster/util_test.go +++ b/pkg/cluster/util_test.go @@ -299,7 +299,7 @@ func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster, OpConfig: config.Config{ PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second}, PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second}, - KubernetesUseConfigMaps: true, + KubernetesUseConfigMaps: util.True(), ConnectionPooler: config.ConnectionPooler{ ConnectionPoolerDefaultCPURequest: "100m", ConnectionPoolerDefaultCPULimit: "100m", @@ -388,7 +388,7 @@ func createPatroniResources(cluster *Cluster) error { Labels: cluster.labelsSet(false), } - if cluster.OpConfig.KubernetesUseConfigMaps { + if cluster.OpConfig.KubernetesUseConfigMaps != nil && *cluster.OpConfig.KubernetesUseConfigMaps { configMap := v1.ConfigMap{ ObjectMeta: metadata, } @@ -598,7 +598,7 @@ func TestInheritedAnnotations(t *testing.T) { // 3. Change from ConfigMaps to Endpoints err = cluster.deletePatroniResources() assert.NoError(t, err) - cluster.OpConfig.KubernetesUseConfigMaps = false + cluster.OpConfig.KubernetesUseConfigMaps = util.False() err = createPatroniResources(cluster) assert.NoError(t, err) err = cluster.Sync(newSpec.DeepCopy()) diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 55f1f00d8..b3765e387 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -35,7 +35,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.EnableSpiloWalPathCompat = fromCRD.EnableSpiloWalPathCompat result.EnableTeamIdClusternamePrefix = fromCRD.EnableTeamIdClusternamePrefix result.EtcdHost = fromCRD.EtcdHost - result.KubernetesUseConfigMaps = fromCRD.KubernetesUseConfigMaps + result.KubernetesUseConfigMaps = util.CoalesceBool(fromCRD.KubernetesUseConfigMaps, util.True()) result.DockerImage = util.Coalesce(fromCRD.DockerImage, "ghcr.io/zalando/spilo-18:4.1-p1") result.Workers = util.CoalesceUInt32(fromCRD.Workers, 8) result.MinInstances = fromCRD.MinInstances diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 85119b18b..14ae537cc 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -177,7 +177,7 @@ type Config struct { ConnectionPooler 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"` + KubernetesUseConfigMaps *bool `name:"kubernetes_use_configmaps" default:"true"` EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS EnableMaintenanceWindows *bool `name:"enable_maintenance_windows" default:"true"` MaintenanceWindows []string `name:"maintenance_windows"` From 18e359b995985c7dacf90f02fa03ab083e01a31d Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Thu, 2 Jul 2026 16:53:03 +0200 Subject: [PATCH 41/70] drop deprecated fields from Postgresql CRD (#3106) * remove deprecated fields from postgresql CRD --------- Co-authored-by: ida-novindasari_zse Co-authored-by: Mikkel Oscar Lyderik Larsen Co-authored-by: idanovinda --- .../postgres-operator/crds/postgresqls.yaml | 1533 ----------------- docs/migrate.md | 14 +- manifests/postgresql.crd.yaml | 1533 ----------------- pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 1533 ----------------- pkg/apis/acid.zalan.do/v1/postgresql_type.go | 11 - pkg/apis/acid.zalan.do/v1/util_test.go | 162 +- .../acid.zalan.do/v1/zz_generated.deepcopy.go | 17 - pkg/cluster/cluster.go | 2 +- pkg/cluster/k8sres.go | 22 - pkg/controller/postgresql.go | 53 +- pkg/controller/postgresql_test.go | 29 - 11 files changed, 23 insertions(+), 4886 deletions(-) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index cea738dec..94db2d442 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -454,1528 +454,6 @@ spec: - name type: object type: array - init_containers: - description: deprecated - items: - description: A single application container that you want to run - within a pod. - properties: - args: - description: |- - Arguments to the entrypoint. - The container image's CMD is used if this is not provided. - Variable references $(VAR_NAME) are expanded using the container's environment. If a variable - cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will - produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless - of whether the variable exists or not. Cannot be updated. - More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell - items: - type: string - type: array - x-kubernetes-list-type: atomic - command: - description: |- - Entrypoint array. Not executed within a shell. - The container image's ENTRYPOINT is used if this is not provided. - Variable references $(VAR_NAME) are expanded using the container's environment. If a variable - cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will - produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless - of whether the variable exists or not. Cannot be updated. - More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell - items: - type: string - type: array - x-kubernetes-list-type: atomic - env: - description: |- - List of environment variables to set in the container. - Cannot be updated. - items: - description: EnvVar represents an environment variable present - in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. - Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or - its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing - the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's - namespace - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - envFrom: - description: |- - List of sources to populate environment variables in the container. - The keys defined within a source may consist of any printable ASCII characters except '='. - When a key exists in multiple - sources, the value associated with the last source will take precedence. - Values defined by an Env with a duplicate key will take precedence. - Cannot be updated. - items: - description: EnvFromSource represents the source of a set - of ConfigMaps or Secrets - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap must be - defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: |- - Optional text to prepend to the name of each environment variable. - May consist of any printable ASCII characters except '='. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - type: object - type: array - x-kubernetes-list-type: atomic - image: - description: |- - Container image name. - More info: https://kubernetes.io/docs/concepts/containers/images - This field is optional to allow higher level config management to default or override - container images in workload controllers like Deployments and StatefulSets. - type: string - imagePullPolicy: - description: |- - Image pull policy. - One of Always, Never, IfNotPresent. - Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/containers/images#updating-images - type: string - lifecycle: - description: |- - Actions that the management system should take in response to container lifecycle events. - Cannot be updated. - properties: - postStart: - description: |- - PostStart is called immediately after a container is created. If the handler fails, - the container is terminated and restarted according to its restart policy. - Other management of the container blocks until the hook completes. - More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks - properties: - exec: - description: Exec specifies a command to execute in - the container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to - perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - sleep: - description: Sleep represents a duration that the container - should sleep. - properties: - seconds: - description: Seconds is the number of seconds to - sleep. - format: int64 - type: integer - required: - - seconds - type: object - tcpSocket: - description: |- - Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept - for backward compatibility. There is no validation of this field and - lifecycle hooks will fail at runtime when it is specified. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - type: object - preStop: - description: |- - PreStop is called immediately before a container is terminated due to an - API request or management event such as liveness/startup probe failure, - preemption, resource contention, etc. The handler is not called if the - container crashes or exits. The Pod's termination grace period countdown begins before the - PreStop hook is executed. Regardless of the outcome of the handler, the - container will eventually terminate within the Pod's termination grace - period (unless delayed by finalizers). Other management of the container blocks until the hook completes - or until the termination grace period is reached. - More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks - properties: - exec: - description: Exec specifies a command to execute in - the container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to - perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - sleep: - description: Sleep represents a duration that the container - should sleep. - properties: - seconds: - description: Seconds is the number of seconds to - sleep. - format: int64 - type: integer - required: - - seconds - type: object - tcpSocket: - description: |- - Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept - for backward compatibility. There is no validation of this field and - lifecycle hooks will fail at runtime when it is specified. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - type: object - stopSignal: - description: |- - StopSignal defines which signal will be sent to a container when it is being stopped. - If not specified, the default is defined by the container runtime in use. - StopSignal can only be set for Pods with a non-empty .spec.os.name - type: string - type: object - livenessProbe: - description: |- - Periodic probe of container liveness. - Container will be restarted if the probe fails. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - name: - description: |- - Name of the container specified as a DNS_LABEL. - Each container in a pod must have a unique name (DNS_LABEL). - Cannot be updated. - type: string - ports: - description: |- - List of ports to expose from the container. Not specifying a port here - DOES NOT prevent that port from being exposed. Any port which is - listening on the default "0.0.0.0" address inside a container will be - accessible from the network. - Modifying this array with strategic merge patch may corrupt the data. - For more information See https://github.com/kubernetes/kubernetes/issues/108255. - Cannot be updated. - items: - description: ContainerPort represents a network port in a - single container. - properties: - containerPort: - description: |- - Number of port to expose on the pod's IP address. - This must be a valid port number, 0 < x < 65536. - format: int32 - type: integer - hostIP: - description: What host IP to bind the external port to. - type: string - hostPort: - description: |- - Number of port to expose on the host. - If specified, this must be a valid port number, 0 < x < 65536. - If HostNetwork is specified, this must match ContainerPort. - Most containers do not need this. - format: int32 - type: integer - name: - description: |- - If specified, this must be an IANA_SVC_NAME and unique within the pod. Each - named port in a pod must have a unique name. Name for the port that can be - referred to by services. - type: string - protocol: - default: TCP - description: |- - Protocol for port. Must be UDP, TCP, or SCTP. - Defaults to "TCP". - type: string - required: - - containerPort - type: object - type: array - x-kubernetes-list-map-keys: - - containerPort - - protocol - x-kubernetes-list-type: map - readinessProbe: - description: |- - Periodic probe of container service readiness. - Container will be removed from service endpoints if the probe fails. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - resizePolicy: - description: |- - Resources resize policy for the container. - This field cannot be set on ephemeral containers. - items: - description: ContainerResizePolicy represents resource resize - policy for the container. - properties: - resourceName: - description: |- - Name of the resource to which this resource resize policy applies. - Supported values: cpu, memory. - type: string - restartPolicy: - description: |- - Restart policy to apply when specified resource is resized. - If not specified, it defaults to NotRequired. - type: string - required: - - resourceName - - restartPolicy - type: object - type: array - x-kubernetes-list-type: atomic - resources: - description: |- - Compute Resources required by this container. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - restartPolicy: - description: |- - RestartPolicy defines the restart behavior of individual containers in a pod. - This overrides the pod-level restart policy. When this field is not specified, - the restart behavior is defined by the Pod's restart policy and the container type. - Additionally, setting the RestartPolicy as "Always" for the init container will - have the following effect: - this init container will be continually restarted on - exit until all regular containers have terminated. Once all regular - containers have completed, all init containers with restartPolicy "Always" - will be shut down. This lifecycle differs from normal init containers and - is often referred to as a "sidecar" container. Although this init - container still starts in the init container sequence, it does not wait - for the container to complete before proceeding to the next init - container. Instead, the next init container starts immediately after this - init container is started, or after any startupProbe has successfully - completed. - type: string - restartPolicyRules: - description: |- - Represents a list of rules to be checked to determine if the - container should be restarted on exit. The rules are evaluated in - order. Once a rule matches a container exit condition, the remaining - rules are ignored. If no rule matches the container exit condition, - the Container-level restart policy determines the whether the container - is restarted or not. Constraints on the rules: - - At most 20 rules are allowed. - - Rules can have the same action. - - Identical rules are not forbidden in validations. - When rules are specified, container MUST set RestartPolicy explicitly - even it if matches the Pod's RestartPolicy. - items: - description: ContainerRestartRule describes how a container - exit is handled. - properties: - action: - description: |- - Specifies the action taken on a container exit if the requirements - are satisfied. The only possible value is "Restart" to restart the - container. - type: string - exitCodes: - description: Represents the exit codes to check on container - exits. - properties: - operator: - description: |- - Represents the relationship between the container exit code(s) and the - specified values. Possible values are: - - In: the requirement is satisfied if the container exit code is in the - set of specified values. - - NotIn: the requirement is satisfied if the container exit code is - not in the set of specified values. - type: string - values: - description: |- - Specifies the set of values to check for container exit codes. - At most 255 elements are allowed. - items: - format: int32 - type: integer - type: array - x-kubernetes-list-type: set - required: - - operator - type: object - required: - - action - type: object - type: array - x-kubernetes-list-type: atomic - securityContext: - description: |- - SecurityContext defines the security options the container should be run with. - If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. - More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ - properties: - allowPrivilegeEscalation: - description: |- - AllowPrivilegeEscalation controls whether a process can gain more - privileges than its parent process. This bool directly controls if - the no_new_privs flag will be set on the container process. - AllowPrivilegeEscalation is true always when the container is: - 1) run as Privileged - 2) has CAP_SYS_ADMIN - Note that this field cannot be set when spec.os.name is windows. - type: boolean - appArmorProfile: - description: |- - appArmorProfile is the AppArmor options to use by this container. If set, this profile - overrides the pod's appArmorProfile. - Note that this field cannot be set when spec.os.name is windows. - properties: - localhostProfile: - description: |- - localhostProfile indicates a profile loaded on the node that should be used. - The profile must be preconfigured on the node to work. - Must match the loaded name of the profile. - Must be set if and only if type is "Localhost". - type: string - type: - description: |- - type indicates which kind of AppArmor profile will be applied. - Valid options are: - Localhost - a profile pre-loaded on the node. - RuntimeDefault - the container runtime's default profile. - Unconfined - no AppArmor enforcement. - type: string - required: - - type - type: object - capabilities: - description: |- - The capabilities to add/drop when running containers. - Defaults to the default set of capabilities granted by the container runtime. - Note that this field cannot be set when spec.os.name is windows. - properties: - add: - description: Added capabilities - items: - description: Capability represent POSIX capabilities - type - type: string - type: array - x-kubernetes-list-type: atomic - drop: - description: Removed capabilities - items: - description: Capability represent POSIX capabilities - type - type: string - type: array - x-kubernetes-list-type: atomic - type: object - privileged: - description: |- - Run container in privileged mode. - Processes in privileged containers are essentially equivalent to root on the host. - Defaults to false. - Note that this field cannot be set when spec.os.name is windows. - type: boolean - procMount: - description: |- - procMount denotes the type of proc mount to use for the containers. - The default value is Default which uses the container runtime defaults for - readonly paths and masked paths. - Note that this field cannot be set when spec.os.name is windows. - type: string - readOnlyRootFilesystem: - description: |- - Whether this container has a read-only root filesystem. - Default is false. - Note that this field cannot be set when spec.os.name is windows. - type: boolean - runAsGroup: - description: |- - The GID to run the entrypoint of the container process. - Uses runtime default if unset. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - runAsNonRoot: - description: |- - Indicates that the container must run as a non-root user. - If true, the Kubelet will validate the image at runtime to ensure that it - does not run as UID 0 (root) and fail to start the container if it does. - If unset or false, no such validation will be performed. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - type: boolean - runAsUser: - description: |- - The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - seLinuxOptions: - description: |- - The SELinux context to be applied to the container. - If unspecified, the container runtime will allocate a random SELinux context for each - container. May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - properties: - level: - description: Level is SELinux level label that applies - to the container. - type: string - role: - description: Role is a SELinux role label that applies - to the container. - type: string - type: - description: Type is a SELinux type label that applies - to the container. - type: string - user: - description: User is a SELinux user label that applies - to the container. - type: string - type: object - seccompProfile: - description: |- - The seccomp options to use by this container. If seccomp options are - provided at both the pod & container level, the container options - override the pod options. - Note that this field cannot be set when spec.os.name is windows. - properties: - localhostProfile: - description: |- - localhostProfile indicates a profile defined in a file on the node should be used. - The profile must be preconfigured on the node to work. - Must be a descending path, relative to the kubelet's configured seccomp profile location. - Must be set if type is "Localhost". Must NOT be set for any other type. - type: string - type: - description: |- - type indicates which kind of seccomp profile will be applied. - Valid options are: - - Localhost - a profile defined in a file on the node should be used. - RuntimeDefault - the container runtime default profile should be used. - Unconfined - no profile should be applied. - type: string - required: - - type - type: object - windowsOptions: - description: |- - The Windows specific settings applied to all containers. - If unspecified, the options from the PodSecurityContext will be used. - If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is linux. - properties: - gmsaCredentialSpec: - description: |- - GMSACredentialSpec is where the GMSA admission webhook - (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the - GMSA credential spec named by the GMSACredentialSpecName field. - type: string - gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the - GMSA credential spec to use. - type: string - hostProcess: - description: |- - HostProcess determines if a container should be run as a 'Host Process' container. - All of a Pod's containers must have the same effective HostProcess value - (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). - In addition, if HostProcess is true then HostNetwork must also be set to true. - type: boolean - runAsUserName: - description: |- - The UserName in Windows to run the entrypoint of the container process. - Defaults to the user specified in image metadata if unspecified. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - type: string - type: object - type: object - startupProbe: - description: |- - StartupProbe indicates that the Pod has successfully initialized. - If specified, no other probes are executed until this completes successfully. - If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. - This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, - when it might take a long time to load data or warm a cache, than during steady-state operation. - This cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - stdin: - description: |- - Whether this container should allocate a buffer for stdin in the container runtime. If this - is not set, reads from stdin in the container will always result in EOF. - Default is false. - type: boolean - stdinOnce: - description: |- - Whether the container runtime should close the stdin channel after it has been opened by - a single attach. When stdin is true the stdin stream will remain open across multiple attach - sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the - first client attaches to stdin, and then remains open and accepts data until the client disconnects, - at which time stdin is closed and remains closed until the container is restarted. If this - flag is false, a container processes that reads from stdin will never receive an EOF. - Default is false - type: boolean - terminationMessagePath: - description: |- - Optional: Path at which the file to which the container's termination message - will be written is mounted into the container's filesystem. - Message written is intended to be brief final status, such as an assertion failure message. - Will be truncated by the node if greater than 4096 bytes. The total message length across - all containers will be limited to 12kb. - Defaults to /dev/termination-log. - Cannot be updated. - type: string - terminationMessagePolicy: - description: |- - Indicate how the termination message should be populated. File will use the contents of - terminationMessagePath to populate the container status message on both success and failure. - FallbackToLogsOnError will use the last chunk of container log output if the termination - message file is empty and the container exited with an error. - The log output is limited to 2048 bytes or 80 lines, whichever is smaller. - Defaults to File. - Cannot be updated. - type: string - tty: - description: |- - Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. - Default is false. - type: boolean - volumeDevices: - description: volumeDevices is the list of block devices to be - used by the container. - items: - description: volumeDevice describes a mapping of a raw block - device within a container. - properties: - devicePath: - description: devicePath is the path inside of the container - that the device will be mapped to. - type: string - name: - description: name must match the name of a persistentVolumeClaim - in the pod - type: string - required: - - devicePath - - name - type: object - type: array - x-kubernetes-list-map-keys: - - devicePath - x-kubernetes-list-type: map - volumeMounts: - description: |- - Pod volumes to mount into the container's filesystem. - Cannot be updated. - items: - description: VolumeMount describes a mounting of a Volume - within a container. - properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - When not set, MountPropagationNone is used. - This field is beta in 1.10. - When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified - (which defaults to None). - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: - description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. - - If ReadOnly is false, this field has no meaning and must be unspecified. - - If ReadOnly is true, and this field is set to Disabled, the mount is not made - recursively read-only. If this field is set to IfPossible, the mount is made - recursively read-only, if it is supported by the container runtime. If this - field is set to Enabled, the mount is made recursively read-only if it is - supported by the container runtime, otherwise the pod will not be started and - an error will be generated to indicate the reason. - - If this field is set to IfPossible or Enabled, MountPropagation must be set to - None (or be unspecified, which defaults to None). - - If this field is not specified, it is treated as an equivalent of Disabled. - type: string - subPath: - description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: |- - Expanded path within the volume from which the container's volume should be mounted. - Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. - Defaults to "" (volume's root). - SubPathExpr and SubPath are mutually exclusive. - type: string - required: - - mountPath - - name - type: object - type: array - x-kubernetes-list-map-keys: - - mountPath - x-kubernetes-list-type: map - workingDir: - description: |- - Container's working directory. - If not specified, the container runtime's default will be used, which - might be configured in the container image. - Cannot be updated. - type: string - required: - - name - type: object - type: array initContainers: items: description: A single application container that you want to run @@ -3914,9 +2392,6 @@ spec: format: int32 type: integer type: object - pod_priority_class_name: - description: deprecated - type: string podAnnotations: additionalProperties: type: string @@ -3967,9 +2442,6 @@ spec: type: string type: object type: object - replicaLoadBalancer: - description: deprecated - type: boolean replicaNodePort: format: int32 type: integer @@ -4684,11 +3156,6 @@ spec: - whenUnsatisfiable type: object type: array - useLoadBalancer: - description: |- - deprecated load balancer settings maintained for backward compatibility - see "Load balancers" operator docs - type: boolean users: additionalProperties: description: UserFlags defines flags (such as superuser, nologin) diff --git a/docs/migrate.md b/docs/migrate.md index 7ec5befb8..e4ccd76b1 100644 --- a/docs/migrate.md +++ b/docs/migrate.md @@ -6,7 +6,7 @@ Version 2.0 changes some default settings and removes deprecated fields. Please If your current operator v1.x deployment is relying on K8s endpoints (the default setup) for Patroni to manage the HA state you have to start planning to switch to configmaps, because endpoints are deprecated from K8s 1.33 onwards. The default of the corresponding parameter `kubernetes_use_configmaps` is changing to `true` with v2.0 of the operator. This means you have to explicity set it to `false` in your configuration before you start the upgrade. -We explicitly warn you to go straight to configmap-based HA management with database clusters that use replicas, because there's is a danger to run into split-brain scenarios during the rolling update of pods. To play it safe, here is what you should do - before or after the Postgres Operator upgrade: +We explicitly warn you to go straight to configmap-based HA management with database clusters that use replicas, because there's is a danger to run into split-brain scenarios during the rolling update of pods when there exists a leader endpoint and leader config map at the same time. To play it safe, here is what you should do - before or after the Postgres Operator upgrade: 1. Scale-in all your database clusters to only one primary instance. This can be done by changing the global config options `max_instances` and `min_instances` to `1`. If you have allowed users to ignore globally defined instance limits by configuring an `ignore_instance_limits_annotation_key`, remove it for now. @@ -15,3 +15,15 @@ We explicitly warn you to go straight to configmap-based HA management with data 3. Check again that all clusters are healthy with configmaps created. There should be three for each cluster called like cluster name with suffixes `-config`, `-failover` and `-leader`. Now, revert the changes from step 1 and scale-out the to number of instances set in the manifests. 4. The orphaned endpoints, which use the same names like the new configmaps, have to be deleted by you or your K8s garbage collection. + + +## Dropped manifest fields + +We removed some deprecated fields from the Postgresql CRD. Please, make sure that you do not specify them in any of your cluster manifests. If you do, switch to the listed alternative: + +| Removed field in v2 | Alternative | +| --- | --- | +| init_containers | initContainers | +| pod_priority_class_name | podPriorityClassName | +| replicaLoadBalancer | enableReplicaLoadBalancer| +| useLoadBalancer | enableMasterLoadBalancer | diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index cea738dec..94db2d442 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -454,1528 +454,6 @@ spec: - name type: object type: array - init_containers: - description: deprecated - items: - description: A single application container that you want to run - within a pod. - properties: - args: - description: |- - Arguments to the entrypoint. - The container image's CMD is used if this is not provided. - Variable references $(VAR_NAME) are expanded using the container's environment. If a variable - cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will - produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless - of whether the variable exists or not. Cannot be updated. - More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell - items: - type: string - type: array - x-kubernetes-list-type: atomic - command: - description: |- - Entrypoint array. Not executed within a shell. - The container image's ENTRYPOINT is used if this is not provided. - Variable references $(VAR_NAME) are expanded using the container's environment. If a variable - cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will - produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless - of whether the variable exists or not. Cannot be updated. - More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell - items: - type: string - type: array - x-kubernetes-list-type: atomic - env: - description: |- - List of environment variables to set in the container. - Cannot be updated. - items: - description: EnvVar represents an environment variable present - in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. - Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or - its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing - the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's - namespace - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - envFrom: - description: |- - List of sources to populate environment variables in the container. - The keys defined within a source may consist of any printable ASCII characters except '='. - When a key exists in multiple - sources, the value associated with the last source will take precedence. - Values defined by an Env with a duplicate key will take precedence. - Cannot be updated. - items: - description: EnvFromSource represents the source of a set - of ConfigMaps or Secrets - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap must be - defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: |- - Optional text to prepend to the name of each environment variable. - May consist of any printable ASCII characters except '='. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - type: object - type: array - x-kubernetes-list-type: atomic - image: - description: |- - Container image name. - More info: https://kubernetes.io/docs/concepts/containers/images - This field is optional to allow higher level config management to default or override - container images in workload controllers like Deployments and StatefulSets. - type: string - imagePullPolicy: - description: |- - Image pull policy. - One of Always, Never, IfNotPresent. - Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/containers/images#updating-images - type: string - lifecycle: - description: |- - Actions that the management system should take in response to container lifecycle events. - Cannot be updated. - properties: - postStart: - description: |- - PostStart is called immediately after a container is created. If the handler fails, - the container is terminated and restarted according to its restart policy. - Other management of the container blocks until the hook completes. - More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks - properties: - exec: - description: Exec specifies a command to execute in - the container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to - perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - sleep: - description: Sleep represents a duration that the container - should sleep. - properties: - seconds: - description: Seconds is the number of seconds to - sleep. - format: int64 - type: integer - required: - - seconds - type: object - tcpSocket: - description: |- - Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept - for backward compatibility. There is no validation of this field and - lifecycle hooks will fail at runtime when it is specified. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - type: object - preStop: - description: |- - PreStop is called immediately before a container is terminated due to an - API request or management event such as liveness/startup probe failure, - preemption, resource contention, etc. The handler is not called if the - container crashes or exits. The Pod's termination grace period countdown begins before the - PreStop hook is executed. Regardless of the outcome of the handler, the - container will eventually terminate within the Pod's termination grace - period (unless delayed by finalizers). Other management of the container blocks until the hook completes - or until the termination grace period is reached. - More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks - properties: - exec: - description: Exec specifies a command to execute in - the container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to - perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - sleep: - description: Sleep represents a duration that the container - should sleep. - properties: - seconds: - description: Seconds is the number of seconds to - sleep. - format: int64 - type: integer - required: - - seconds - type: object - tcpSocket: - description: |- - Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept - for backward compatibility. There is no validation of this field and - lifecycle hooks will fail at runtime when it is specified. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - type: object - stopSignal: - description: |- - StopSignal defines which signal will be sent to a container when it is being stopped. - If not specified, the default is defined by the container runtime in use. - StopSignal can only be set for Pods with a non-empty .spec.os.name - type: string - type: object - livenessProbe: - description: |- - Periodic probe of container liveness. - Container will be restarted if the probe fails. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - name: - description: |- - Name of the container specified as a DNS_LABEL. - Each container in a pod must have a unique name (DNS_LABEL). - Cannot be updated. - type: string - ports: - description: |- - List of ports to expose from the container. Not specifying a port here - DOES NOT prevent that port from being exposed. Any port which is - listening on the default "0.0.0.0" address inside a container will be - accessible from the network. - Modifying this array with strategic merge patch may corrupt the data. - For more information See https://github.com/kubernetes/kubernetes/issues/108255. - Cannot be updated. - items: - description: ContainerPort represents a network port in a - single container. - properties: - containerPort: - description: |- - Number of port to expose on the pod's IP address. - This must be a valid port number, 0 < x < 65536. - format: int32 - type: integer - hostIP: - description: What host IP to bind the external port to. - type: string - hostPort: - description: |- - Number of port to expose on the host. - If specified, this must be a valid port number, 0 < x < 65536. - If HostNetwork is specified, this must match ContainerPort. - Most containers do not need this. - format: int32 - type: integer - name: - description: |- - If specified, this must be an IANA_SVC_NAME and unique within the pod. Each - named port in a pod must have a unique name. Name for the port that can be - referred to by services. - type: string - protocol: - default: TCP - description: |- - Protocol for port. Must be UDP, TCP, or SCTP. - Defaults to "TCP". - type: string - required: - - containerPort - type: object - type: array - x-kubernetes-list-map-keys: - - containerPort - - protocol - x-kubernetes-list-type: map - readinessProbe: - description: |- - Periodic probe of container service readiness. - Container will be removed from service endpoints if the probe fails. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - resizePolicy: - description: |- - Resources resize policy for the container. - This field cannot be set on ephemeral containers. - items: - description: ContainerResizePolicy represents resource resize - policy for the container. - properties: - resourceName: - description: |- - Name of the resource to which this resource resize policy applies. - Supported values: cpu, memory. - type: string - restartPolicy: - description: |- - Restart policy to apply when specified resource is resized. - If not specified, it defaults to NotRequired. - type: string - required: - - resourceName - - restartPolicy - type: object - type: array - x-kubernetes-list-type: atomic - resources: - description: |- - Compute Resources required by this container. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - restartPolicy: - description: |- - RestartPolicy defines the restart behavior of individual containers in a pod. - This overrides the pod-level restart policy. When this field is not specified, - the restart behavior is defined by the Pod's restart policy and the container type. - Additionally, setting the RestartPolicy as "Always" for the init container will - have the following effect: - this init container will be continually restarted on - exit until all regular containers have terminated. Once all regular - containers have completed, all init containers with restartPolicy "Always" - will be shut down. This lifecycle differs from normal init containers and - is often referred to as a "sidecar" container. Although this init - container still starts in the init container sequence, it does not wait - for the container to complete before proceeding to the next init - container. Instead, the next init container starts immediately after this - init container is started, or after any startupProbe has successfully - completed. - type: string - restartPolicyRules: - description: |- - Represents a list of rules to be checked to determine if the - container should be restarted on exit. The rules are evaluated in - order. Once a rule matches a container exit condition, the remaining - rules are ignored. If no rule matches the container exit condition, - the Container-level restart policy determines the whether the container - is restarted or not. Constraints on the rules: - - At most 20 rules are allowed. - - Rules can have the same action. - - Identical rules are not forbidden in validations. - When rules are specified, container MUST set RestartPolicy explicitly - even it if matches the Pod's RestartPolicy. - items: - description: ContainerRestartRule describes how a container - exit is handled. - properties: - action: - description: |- - Specifies the action taken on a container exit if the requirements - are satisfied. The only possible value is "Restart" to restart the - container. - type: string - exitCodes: - description: Represents the exit codes to check on container - exits. - properties: - operator: - description: |- - Represents the relationship between the container exit code(s) and the - specified values. Possible values are: - - In: the requirement is satisfied if the container exit code is in the - set of specified values. - - NotIn: the requirement is satisfied if the container exit code is - not in the set of specified values. - type: string - values: - description: |- - Specifies the set of values to check for container exit codes. - At most 255 elements are allowed. - items: - format: int32 - type: integer - type: array - x-kubernetes-list-type: set - required: - - operator - type: object - required: - - action - type: object - type: array - x-kubernetes-list-type: atomic - securityContext: - description: |- - SecurityContext defines the security options the container should be run with. - If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. - More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ - properties: - allowPrivilegeEscalation: - description: |- - AllowPrivilegeEscalation controls whether a process can gain more - privileges than its parent process. This bool directly controls if - the no_new_privs flag will be set on the container process. - AllowPrivilegeEscalation is true always when the container is: - 1) run as Privileged - 2) has CAP_SYS_ADMIN - Note that this field cannot be set when spec.os.name is windows. - type: boolean - appArmorProfile: - description: |- - appArmorProfile is the AppArmor options to use by this container. If set, this profile - overrides the pod's appArmorProfile. - Note that this field cannot be set when spec.os.name is windows. - properties: - localhostProfile: - description: |- - localhostProfile indicates a profile loaded on the node that should be used. - The profile must be preconfigured on the node to work. - Must match the loaded name of the profile. - Must be set if and only if type is "Localhost". - type: string - type: - description: |- - type indicates which kind of AppArmor profile will be applied. - Valid options are: - Localhost - a profile pre-loaded on the node. - RuntimeDefault - the container runtime's default profile. - Unconfined - no AppArmor enforcement. - type: string - required: - - type - type: object - capabilities: - description: |- - The capabilities to add/drop when running containers. - Defaults to the default set of capabilities granted by the container runtime. - Note that this field cannot be set when spec.os.name is windows. - properties: - add: - description: Added capabilities - items: - description: Capability represent POSIX capabilities - type - type: string - type: array - x-kubernetes-list-type: atomic - drop: - description: Removed capabilities - items: - description: Capability represent POSIX capabilities - type - type: string - type: array - x-kubernetes-list-type: atomic - type: object - privileged: - description: |- - Run container in privileged mode. - Processes in privileged containers are essentially equivalent to root on the host. - Defaults to false. - Note that this field cannot be set when spec.os.name is windows. - type: boolean - procMount: - description: |- - procMount denotes the type of proc mount to use for the containers. - The default value is Default which uses the container runtime defaults for - readonly paths and masked paths. - Note that this field cannot be set when spec.os.name is windows. - type: string - readOnlyRootFilesystem: - description: |- - Whether this container has a read-only root filesystem. - Default is false. - Note that this field cannot be set when spec.os.name is windows. - type: boolean - runAsGroup: - description: |- - The GID to run the entrypoint of the container process. - Uses runtime default if unset. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - runAsNonRoot: - description: |- - Indicates that the container must run as a non-root user. - If true, the Kubelet will validate the image at runtime to ensure that it - does not run as UID 0 (root) and fail to start the container if it does. - If unset or false, no such validation will be performed. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - type: boolean - runAsUser: - description: |- - The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - seLinuxOptions: - description: |- - The SELinux context to be applied to the container. - If unspecified, the container runtime will allocate a random SELinux context for each - container. May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - properties: - level: - description: Level is SELinux level label that applies - to the container. - type: string - role: - description: Role is a SELinux role label that applies - to the container. - type: string - type: - description: Type is a SELinux type label that applies - to the container. - type: string - user: - description: User is a SELinux user label that applies - to the container. - type: string - type: object - seccompProfile: - description: |- - The seccomp options to use by this container. If seccomp options are - provided at both the pod & container level, the container options - override the pod options. - Note that this field cannot be set when spec.os.name is windows. - properties: - localhostProfile: - description: |- - localhostProfile indicates a profile defined in a file on the node should be used. - The profile must be preconfigured on the node to work. - Must be a descending path, relative to the kubelet's configured seccomp profile location. - Must be set if type is "Localhost". Must NOT be set for any other type. - type: string - type: - description: |- - type indicates which kind of seccomp profile will be applied. - Valid options are: - - Localhost - a profile defined in a file on the node should be used. - RuntimeDefault - the container runtime default profile should be used. - Unconfined - no profile should be applied. - type: string - required: - - type - type: object - windowsOptions: - description: |- - The Windows specific settings applied to all containers. - If unspecified, the options from the PodSecurityContext will be used. - If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is linux. - properties: - gmsaCredentialSpec: - description: |- - GMSACredentialSpec is where the GMSA admission webhook - (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the - GMSA credential spec named by the GMSACredentialSpecName field. - type: string - gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the - GMSA credential spec to use. - type: string - hostProcess: - description: |- - HostProcess determines if a container should be run as a 'Host Process' container. - All of a Pod's containers must have the same effective HostProcess value - (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). - In addition, if HostProcess is true then HostNetwork must also be set to true. - type: boolean - runAsUserName: - description: |- - The UserName in Windows to run the entrypoint of the container process. - Defaults to the user specified in image metadata if unspecified. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - type: string - type: object - type: object - startupProbe: - description: |- - StartupProbe indicates that the Pod has successfully initialized. - If specified, no other probes are executed until this completes successfully. - If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. - This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, - when it might take a long time to load data or warm a cache, than during steady-state operation. - This cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - stdin: - description: |- - Whether this container should allocate a buffer for stdin in the container runtime. If this - is not set, reads from stdin in the container will always result in EOF. - Default is false. - type: boolean - stdinOnce: - description: |- - Whether the container runtime should close the stdin channel after it has been opened by - a single attach. When stdin is true the stdin stream will remain open across multiple attach - sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the - first client attaches to stdin, and then remains open and accepts data until the client disconnects, - at which time stdin is closed and remains closed until the container is restarted. If this - flag is false, a container processes that reads from stdin will never receive an EOF. - Default is false - type: boolean - terminationMessagePath: - description: |- - Optional: Path at which the file to which the container's termination message - will be written is mounted into the container's filesystem. - Message written is intended to be brief final status, such as an assertion failure message. - Will be truncated by the node if greater than 4096 bytes. The total message length across - all containers will be limited to 12kb. - Defaults to /dev/termination-log. - Cannot be updated. - type: string - terminationMessagePolicy: - description: |- - Indicate how the termination message should be populated. File will use the contents of - terminationMessagePath to populate the container status message on both success and failure. - FallbackToLogsOnError will use the last chunk of container log output if the termination - message file is empty and the container exited with an error. - The log output is limited to 2048 bytes or 80 lines, whichever is smaller. - Defaults to File. - Cannot be updated. - type: string - tty: - description: |- - Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. - Default is false. - type: boolean - volumeDevices: - description: volumeDevices is the list of block devices to be - used by the container. - items: - description: volumeDevice describes a mapping of a raw block - device within a container. - properties: - devicePath: - description: devicePath is the path inside of the container - that the device will be mapped to. - type: string - name: - description: name must match the name of a persistentVolumeClaim - in the pod - type: string - required: - - devicePath - - name - type: object - type: array - x-kubernetes-list-map-keys: - - devicePath - x-kubernetes-list-type: map - volumeMounts: - description: |- - Pod volumes to mount into the container's filesystem. - Cannot be updated. - items: - description: VolumeMount describes a mounting of a Volume - within a container. - properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - When not set, MountPropagationNone is used. - This field is beta in 1.10. - When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified - (which defaults to None). - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: - description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. - - If ReadOnly is false, this field has no meaning and must be unspecified. - - If ReadOnly is true, and this field is set to Disabled, the mount is not made - recursively read-only. If this field is set to IfPossible, the mount is made - recursively read-only, if it is supported by the container runtime. If this - field is set to Enabled, the mount is made recursively read-only if it is - supported by the container runtime, otherwise the pod will not be started and - an error will be generated to indicate the reason. - - If this field is set to IfPossible or Enabled, MountPropagation must be set to - None (or be unspecified, which defaults to None). - - If this field is not specified, it is treated as an equivalent of Disabled. - type: string - subPath: - description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: |- - Expanded path within the volume from which the container's volume should be mounted. - Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. - Defaults to "" (volume's root). - SubPathExpr and SubPath are mutually exclusive. - type: string - required: - - mountPath - - name - type: object - type: array - x-kubernetes-list-map-keys: - - mountPath - x-kubernetes-list-type: map - workingDir: - description: |- - Container's working directory. - If not specified, the container runtime's default will be used, which - might be configured in the container image. - Cannot be updated. - type: string - required: - - name - type: object - type: array initContainers: items: description: A single application container that you want to run @@ -3914,9 +2392,6 @@ spec: format: int32 type: integer type: object - pod_priority_class_name: - description: deprecated - type: string podAnnotations: additionalProperties: type: string @@ -3967,9 +2442,6 @@ spec: type: string type: object type: object - replicaLoadBalancer: - description: deprecated - type: boolean replicaNodePort: format: int32 type: integer @@ -4684,11 +3156,6 @@ spec: - whenUnsatisfiable type: object type: array - useLoadBalancer: - description: |- - deprecated load balancer settings maintained for backward compatibility - see "Load balancers" operator docs - type: boolean users: additionalProperties: description: UserFlags defines flags (such as superuser, nologin) diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index cea738dec..94db2d442 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -454,1528 +454,6 @@ spec: - name type: object type: array - init_containers: - description: deprecated - items: - description: A single application container that you want to run - within a pod. - properties: - args: - description: |- - Arguments to the entrypoint. - The container image's CMD is used if this is not provided. - Variable references $(VAR_NAME) are expanded using the container's environment. If a variable - cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will - produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless - of whether the variable exists or not. Cannot be updated. - More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell - items: - type: string - type: array - x-kubernetes-list-type: atomic - command: - description: |- - Entrypoint array. Not executed within a shell. - The container image's ENTRYPOINT is used if this is not provided. - Variable references $(VAR_NAME) are expanded using the container's environment. If a variable - cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will - produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless - of whether the variable exists or not. Cannot be updated. - More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell - items: - type: string - type: array - x-kubernetes-list-type: atomic - env: - description: |- - List of environment variables to set in the container. - Cannot be updated. - items: - description: EnvVar represents an environment variable present - in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. - Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or - its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing - the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's - namespace - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - envFrom: - description: |- - List of sources to populate environment variables in the container. - The keys defined within a source may consist of any printable ASCII characters except '='. - When a key exists in multiple - sources, the value associated with the last source will take precedence. - Values defined by an Env with a duplicate key will take precedence. - Cannot be updated. - items: - description: EnvFromSource represents the source of a set - of ConfigMaps or Secrets - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap must be - defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: |- - Optional text to prepend to the name of each environment variable. - May consist of any printable ASCII characters except '='. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - type: object - type: array - x-kubernetes-list-type: atomic - image: - description: |- - Container image name. - More info: https://kubernetes.io/docs/concepts/containers/images - This field is optional to allow higher level config management to default or override - container images in workload controllers like Deployments and StatefulSets. - type: string - imagePullPolicy: - description: |- - Image pull policy. - One of Always, Never, IfNotPresent. - Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/containers/images#updating-images - type: string - lifecycle: - description: |- - Actions that the management system should take in response to container lifecycle events. - Cannot be updated. - properties: - postStart: - description: |- - PostStart is called immediately after a container is created. If the handler fails, - the container is terminated and restarted according to its restart policy. - Other management of the container blocks until the hook completes. - More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks - properties: - exec: - description: Exec specifies a command to execute in - the container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to - perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - sleep: - description: Sleep represents a duration that the container - should sleep. - properties: - seconds: - description: Seconds is the number of seconds to - sleep. - format: int64 - type: integer - required: - - seconds - type: object - tcpSocket: - description: |- - Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept - for backward compatibility. There is no validation of this field and - lifecycle hooks will fail at runtime when it is specified. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - type: object - preStop: - description: |- - PreStop is called immediately before a container is terminated due to an - API request or management event such as liveness/startup probe failure, - preemption, resource contention, etc. The handler is not called if the - container crashes or exits. The Pod's termination grace period countdown begins before the - PreStop hook is executed. Regardless of the outcome of the handler, the - container will eventually terminate within the Pod's termination grace - period (unless delayed by finalizers). Other management of the container blocks until the hook completes - or until the termination grace period is reached. - More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks - properties: - exec: - description: Exec specifies a command to execute in - the container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to - perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - sleep: - description: Sleep represents a duration that the container - should sleep. - properties: - seconds: - description: Seconds is the number of seconds to - sleep. - format: int64 - type: integer - required: - - seconds - type: object - tcpSocket: - description: |- - Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept - for backward compatibility. There is no validation of this field and - lifecycle hooks will fail at runtime when it is specified. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - type: object - stopSignal: - description: |- - StopSignal defines which signal will be sent to a container when it is being stopped. - If not specified, the default is defined by the container runtime in use. - StopSignal can only be set for Pods with a non-empty .spec.os.name - type: string - type: object - livenessProbe: - description: |- - Periodic probe of container liveness. - Container will be restarted if the probe fails. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - name: - description: |- - Name of the container specified as a DNS_LABEL. - Each container in a pod must have a unique name (DNS_LABEL). - Cannot be updated. - type: string - ports: - description: |- - List of ports to expose from the container. Not specifying a port here - DOES NOT prevent that port from being exposed. Any port which is - listening on the default "0.0.0.0" address inside a container will be - accessible from the network. - Modifying this array with strategic merge patch may corrupt the data. - For more information See https://github.com/kubernetes/kubernetes/issues/108255. - Cannot be updated. - items: - description: ContainerPort represents a network port in a - single container. - properties: - containerPort: - description: |- - Number of port to expose on the pod's IP address. - This must be a valid port number, 0 < x < 65536. - format: int32 - type: integer - hostIP: - description: What host IP to bind the external port to. - type: string - hostPort: - description: |- - Number of port to expose on the host. - If specified, this must be a valid port number, 0 < x < 65536. - If HostNetwork is specified, this must match ContainerPort. - Most containers do not need this. - format: int32 - type: integer - name: - description: |- - If specified, this must be an IANA_SVC_NAME and unique within the pod. Each - named port in a pod must have a unique name. Name for the port that can be - referred to by services. - type: string - protocol: - default: TCP - description: |- - Protocol for port. Must be UDP, TCP, or SCTP. - Defaults to "TCP". - type: string - required: - - containerPort - type: object - type: array - x-kubernetes-list-map-keys: - - containerPort - - protocol - x-kubernetes-list-type: map - readinessProbe: - description: |- - Periodic probe of container service readiness. - Container will be removed from service endpoints if the probe fails. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - resizePolicy: - description: |- - Resources resize policy for the container. - This field cannot be set on ephemeral containers. - items: - description: ContainerResizePolicy represents resource resize - policy for the container. - properties: - resourceName: - description: |- - Name of the resource to which this resource resize policy applies. - Supported values: cpu, memory. - type: string - restartPolicy: - description: |- - Restart policy to apply when specified resource is resized. - If not specified, it defaults to NotRequired. - type: string - required: - - resourceName - - restartPolicy - type: object - type: array - x-kubernetes-list-type: atomic - resources: - description: |- - Compute Resources required by this container. - Cannot be updated. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - restartPolicy: - description: |- - RestartPolicy defines the restart behavior of individual containers in a pod. - This overrides the pod-level restart policy. When this field is not specified, - the restart behavior is defined by the Pod's restart policy and the container type. - Additionally, setting the RestartPolicy as "Always" for the init container will - have the following effect: - this init container will be continually restarted on - exit until all regular containers have terminated. Once all regular - containers have completed, all init containers with restartPolicy "Always" - will be shut down. This lifecycle differs from normal init containers and - is often referred to as a "sidecar" container. Although this init - container still starts in the init container sequence, it does not wait - for the container to complete before proceeding to the next init - container. Instead, the next init container starts immediately after this - init container is started, or after any startupProbe has successfully - completed. - type: string - restartPolicyRules: - description: |- - Represents a list of rules to be checked to determine if the - container should be restarted on exit. The rules are evaluated in - order. Once a rule matches a container exit condition, the remaining - rules are ignored. If no rule matches the container exit condition, - the Container-level restart policy determines the whether the container - is restarted or not. Constraints on the rules: - - At most 20 rules are allowed. - - Rules can have the same action. - - Identical rules are not forbidden in validations. - When rules are specified, container MUST set RestartPolicy explicitly - even it if matches the Pod's RestartPolicy. - items: - description: ContainerRestartRule describes how a container - exit is handled. - properties: - action: - description: |- - Specifies the action taken on a container exit if the requirements - are satisfied. The only possible value is "Restart" to restart the - container. - type: string - exitCodes: - description: Represents the exit codes to check on container - exits. - properties: - operator: - description: |- - Represents the relationship between the container exit code(s) and the - specified values. Possible values are: - - In: the requirement is satisfied if the container exit code is in the - set of specified values. - - NotIn: the requirement is satisfied if the container exit code is - not in the set of specified values. - type: string - values: - description: |- - Specifies the set of values to check for container exit codes. - At most 255 elements are allowed. - items: - format: int32 - type: integer - type: array - x-kubernetes-list-type: set - required: - - operator - type: object - required: - - action - type: object - type: array - x-kubernetes-list-type: atomic - securityContext: - description: |- - SecurityContext defines the security options the container should be run with. - If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. - More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ - properties: - allowPrivilegeEscalation: - description: |- - AllowPrivilegeEscalation controls whether a process can gain more - privileges than its parent process. This bool directly controls if - the no_new_privs flag will be set on the container process. - AllowPrivilegeEscalation is true always when the container is: - 1) run as Privileged - 2) has CAP_SYS_ADMIN - Note that this field cannot be set when spec.os.name is windows. - type: boolean - appArmorProfile: - description: |- - appArmorProfile is the AppArmor options to use by this container. If set, this profile - overrides the pod's appArmorProfile. - Note that this field cannot be set when spec.os.name is windows. - properties: - localhostProfile: - description: |- - localhostProfile indicates a profile loaded on the node that should be used. - The profile must be preconfigured on the node to work. - Must match the loaded name of the profile. - Must be set if and only if type is "Localhost". - type: string - type: - description: |- - type indicates which kind of AppArmor profile will be applied. - Valid options are: - Localhost - a profile pre-loaded on the node. - RuntimeDefault - the container runtime's default profile. - Unconfined - no AppArmor enforcement. - type: string - required: - - type - type: object - capabilities: - description: |- - The capabilities to add/drop when running containers. - Defaults to the default set of capabilities granted by the container runtime. - Note that this field cannot be set when spec.os.name is windows. - properties: - add: - description: Added capabilities - items: - description: Capability represent POSIX capabilities - type - type: string - type: array - x-kubernetes-list-type: atomic - drop: - description: Removed capabilities - items: - description: Capability represent POSIX capabilities - type - type: string - type: array - x-kubernetes-list-type: atomic - type: object - privileged: - description: |- - Run container in privileged mode. - Processes in privileged containers are essentially equivalent to root on the host. - Defaults to false. - Note that this field cannot be set when spec.os.name is windows. - type: boolean - procMount: - description: |- - procMount denotes the type of proc mount to use for the containers. - The default value is Default which uses the container runtime defaults for - readonly paths and masked paths. - Note that this field cannot be set when spec.os.name is windows. - type: string - readOnlyRootFilesystem: - description: |- - Whether this container has a read-only root filesystem. - Default is false. - Note that this field cannot be set when spec.os.name is windows. - type: boolean - runAsGroup: - description: |- - The GID to run the entrypoint of the container process. - Uses runtime default if unset. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - runAsNonRoot: - description: |- - Indicates that the container must run as a non-root user. - If true, the Kubelet will validate the image at runtime to ensure that it - does not run as UID 0 (root) and fail to start the container if it does. - If unset or false, no such validation will be performed. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - type: boolean - runAsUser: - description: |- - The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - seLinuxOptions: - description: |- - The SELinux context to be applied to the container. - If unspecified, the container runtime will allocate a random SELinux context for each - container. May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is windows. - properties: - level: - description: Level is SELinux level label that applies - to the container. - type: string - role: - description: Role is a SELinux role label that applies - to the container. - type: string - type: - description: Type is a SELinux type label that applies - to the container. - type: string - user: - description: User is a SELinux user label that applies - to the container. - type: string - type: object - seccompProfile: - description: |- - The seccomp options to use by this container. If seccomp options are - provided at both the pod & container level, the container options - override the pod options. - Note that this field cannot be set when spec.os.name is windows. - properties: - localhostProfile: - description: |- - localhostProfile indicates a profile defined in a file on the node should be used. - The profile must be preconfigured on the node to work. - Must be a descending path, relative to the kubelet's configured seccomp profile location. - Must be set if type is "Localhost". Must NOT be set for any other type. - type: string - type: - description: |- - type indicates which kind of seccomp profile will be applied. - Valid options are: - - Localhost - a profile defined in a file on the node should be used. - RuntimeDefault - the container runtime default profile should be used. - Unconfined - no profile should be applied. - type: string - required: - - type - type: object - windowsOptions: - description: |- - The Windows specific settings applied to all containers. - If unspecified, the options from the PodSecurityContext will be used. - If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is linux. - properties: - gmsaCredentialSpec: - description: |- - GMSACredentialSpec is where the GMSA admission webhook - (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the - GMSA credential spec named by the GMSACredentialSpecName field. - type: string - gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the - GMSA credential spec to use. - type: string - hostProcess: - description: |- - HostProcess determines if a container should be run as a 'Host Process' container. - All of a Pod's containers must have the same effective HostProcess value - (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). - In addition, if HostProcess is true then HostNetwork must also be set to true. - type: boolean - runAsUserName: - description: |- - The UserName in Windows to run the entrypoint of the container process. - Defaults to the user specified in image metadata if unspecified. - May also be set in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext takes precedence. - type: string - type: object - type: object - startupProbe: - description: |- - StartupProbe indicates that the Pod has successfully initialized. - If specified, no other probes are executed until this completes successfully. - If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. - This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, - when it might take a long time to load data or warm a cache, than during steady-state operation. - This cannot be updated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - properties: - exec: - description: Exec specifies a command to execute in the - container. - properties: - command: - description: |- - Command is the command line to execute inside the container, the working directory for the - command is root ('/') in the container's filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use - a shell, you need to explicitly call out to that shell. - Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - failureThreshold: - description: |- - Minimum consecutive failures for the probe to be considered failed after having succeeded. - Defaults to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies a GRPC HealthCheckRequest. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - default: "" - description: |- - Service is the name of the service to place in the gRPC HealthCheckRequest - (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - - If this is not specified, the default behavior is defined by gRPC. - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies an HTTP GET request to perform. - properties: - host: - description: |- - Host name to connect to, defaults to the pod IP. You probably want to set - "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP - allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: |- - The header field name. - This will be canonicalized upon output, so case-variant names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Name or number of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: |- - Scheme to use for connecting to the host. - Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: |- - Number of seconds after the container has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - periodSeconds: - description: |- - How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: |- - Minimum consecutive successes for the probe to be considered successful after having failed. - Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies a connection to a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the container. - Number must be in the range 1 to 65535. - Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: |- - Optional duration in seconds the pod needs to terminate gracefully upon probe failure. - The grace period is the duration in seconds after the processes running in the pod are sent - a termination signal and the time when the processes are forcibly halted with a kill signal. - Set this value longer than the expected cleanup time for your process. - If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. - Value must be non-negative integer. The value zero indicates stop immediately via - the kill signal (no opportunity to shut down). - This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. - Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: |- - Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes - format: int32 - type: integer - type: object - stdin: - description: |- - Whether this container should allocate a buffer for stdin in the container runtime. If this - is not set, reads from stdin in the container will always result in EOF. - Default is false. - type: boolean - stdinOnce: - description: |- - Whether the container runtime should close the stdin channel after it has been opened by - a single attach. When stdin is true the stdin stream will remain open across multiple attach - sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the - first client attaches to stdin, and then remains open and accepts data until the client disconnects, - at which time stdin is closed and remains closed until the container is restarted. If this - flag is false, a container processes that reads from stdin will never receive an EOF. - Default is false - type: boolean - terminationMessagePath: - description: |- - Optional: Path at which the file to which the container's termination message - will be written is mounted into the container's filesystem. - Message written is intended to be brief final status, such as an assertion failure message. - Will be truncated by the node if greater than 4096 bytes. The total message length across - all containers will be limited to 12kb. - Defaults to /dev/termination-log. - Cannot be updated. - type: string - terminationMessagePolicy: - description: |- - Indicate how the termination message should be populated. File will use the contents of - terminationMessagePath to populate the container status message on both success and failure. - FallbackToLogsOnError will use the last chunk of container log output if the termination - message file is empty and the container exited with an error. - The log output is limited to 2048 bytes or 80 lines, whichever is smaller. - Defaults to File. - Cannot be updated. - type: string - tty: - description: |- - Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. - Default is false. - type: boolean - volumeDevices: - description: volumeDevices is the list of block devices to be - used by the container. - items: - description: volumeDevice describes a mapping of a raw block - device within a container. - properties: - devicePath: - description: devicePath is the path inside of the container - that the device will be mapped to. - type: string - name: - description: name must match the name of a persistentVolumeClaim - in the pod - type: string - required: - - devicePath - - name - type: object - type: array - x-kubernetes-list-map-keys: - - devicePath - x-kubernetes-list-type: map - volumeMounts: - description: |- - Pod volumes to mount into the container's filesystem. - Cannot be updated. - items: - description: VolumeMount describes a mounting of a Volume - within a container. - properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - When not set, MountPropagationNone is used. - This field is beta in 1.10. - When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified - (which defaults to None). - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: - description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. - - If ReadOnly is false, this field has no meaning and must be unspecified. - - If ReadOnly is true, and this field is set to Disabled, the mount is not made - recursively read-only. If this field is set to IfPossible, the mount is made - recursively read-only, if it is supported by the container runtime. If this - field is set to Enabled, the mount is made recursively read-only if it is - supported by the container runtime, otherwise the pod will not be started and - an error will be generated to indicate the reason. - - If this field is set to IfPossible or Enabled, MountPropagation must be set to - None (or be unspecified, which defaults to None). - - If this field is not specified, it is treated as an equivalent of Disabled. - type: string - subPath: - description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: |- - Expanded path within the volume from which the container's volume should be mounted. - Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. - Defaults to "" (volume's root). - SubPathExpr and SubPath are mutually exclusive. - type: string - required: - - mountPath - - name - type: object - type: array - x-kubernetes-list-map-keys: - - mountPath - x-kubernetes-list-type: map - workingDir: - description: |- - Container's working directory. - If not specified, the container runtime's default will be used, which - might be configured in the container image. - Cannot be updated. - type: string - required: - - name - type: object - type: array initContainers: items: description: A single application container that you want to run @@ -3914,9 +2392,6 @@ spec: format: int32 type: integer type: object - pod_priority_class_name: - description: deprecated - type: string podAnnotations: additionalProperties: type: string @@ -3967,9 +2442,6 @@ spec: type: string type: object type: object - replicaLoadBalancer: - description: deprecated - type: boolean replicaNodePort: format: int32 type: integer @@ -4684,11 +3156,6 @@ spec: - whenUnsatisfiable type: object type: array - useLoadBalancer: - description: |- - deprecated load balancer settings maintained for backward compatibility - see "Load balancers" operator docs - type: boolean users: additionalProperties: description: UserFlags defines flags (such as superuser, nologin) diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 85a73605c..52d51cd41 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -76,12 +76,6 @@ type PostgresSpec struct { EnableReplicaPoolerNodePort *bool `json:"enableReplicaPoolerNodePort,omitempty"` ReplicaPoolerNodePort *int32 `json:"replicaPoolerNodePort,omitempty"` - // deprecated load balancer settings maintained for backward compatibility - // see "Load balancers" operator docs - UseLoadBalancer *bool `json:"useLoadBalancer,omitempty"` - // deprecated - ReplicaLoadBalancer *bool `json:"replicaLoadBalancer,omitempty"` - // load balancers' source ranges are the same for master and replica services // +nullable // +kubebuilder:validation:items:Pattern=`^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$` @@ -130,11 +124,6 @@ type PostgresSpec struct { AdditionalVolumes []AdditionalVolume `json:"additionalVolumes,omitempty"` Streams []Stream `json:"streams,omitempty"` Env []v1.EnvVar `json:"env,omitempty"` - - // deprecated - InitContainersOld []v1.Container `json:"init_containers,omitempty"` - // deprecated - PodPriorityClassNameOld string `json:"pod_priority_class_name,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/acid.zalan.do/v1/util_test.go b/pkg/apis/acid.zalan.do/v1/util_test.go index 857622581..bf9267fb6 100644 --- a/pkg/apis/acid.zalan.do/v1/util_test.go +++ b/pkg/apis/acid.zalan.do/v1/util_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/zalando/postgres-operator/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -178,7 +177,8 @@ var unmarshalCluster = []struct { "metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(), }, marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":"Invalid"}`), - err: nil}, + err: nil, + }, { about: "example with /status subresource", in: []byte(`{ @@ -199,156 +199,8 @@ var unmarshalCluster = []struct { "metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(), }, marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`), - err: nil}, - { - about: "example with detailed input manifest and deprecated pod_priority_class_name -> podPriorityClassName", - in: []byte(`{ - "kind": "Postgresql", - "apiVersion": "acid.zalan.do/v1", - "metadata": { - "name": "acid-testcluster1" - }, - "spec": { - "teamId": "acid", - "pod_priority_class_name": "spilo-pod-priority", - "volume": { - "size": "5Gi", - "storageClass": "SSD", - "subPath": "subdir" - }, - "numberOfInstances": 2, - "users": { - "zalando": [ - "superuser", - "createdb" - ] - }, - "allowedSourceRanges": [ - "127.0.0.1/32" - ], - "postgresql": { - "version": "18", - "parameters": { - "shared_buffers": "32MB", - "max_connections": "10", - "log_statement": "all" - } - }, - "resources": { - "requests": { - "cpu": "10m", - "memory": "50Mi" - }, - "limits": { - "cpu": "300m", - "memory": "3000Mi" - } - }, - "clone" : { - "cluster": "acid-batman" - }, - "enableShmVolume": false, - "patroni": { - "initdb": { - "encoding": "UTF8", - "locale": "en_US.UTF-8", - "data-checksums": "true" - }, - "pg_hba": [ - "hostssl all all 0.0.0.0/0 md5", - "host all all 0.0.0.0/0 md5" - ], - "ttl": 30, - "loop_wait": 10, - "retry_timeout": 10, - "maximum_lag_on_failover": 33554432, - "slots" : { - "permanent_logical_1" : { - "type" : "logical", - "database" : "foo", - "plugin" : "pgoutput" - } - } - }, - "maintenanceWindows": [ - "Mon:01:00-06:00", - "Sat:00:00-04:00", - "05:00-05:15" - ] - } - }`), - out: Postgresql{ - TypeMeta: metav1.TypeMeta{ - Kind: "Postgresql", - APIVersion: "acid.zalan.do/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "acid-testcluster1", - }, - Spec: PostgresSpec{ - PostgresqlParam: PostgresqlParam{ - PgVersion: "18", - Parameters: map[string]string{ - "shared_buffers": "32MB", - "max_connections": "10", - "log_statement": "all", - }, - }, - PodPriorityClassNameOld: "spilo-pod-priority", - Volume: Volume{ - Size: "5Gi", - StorageClass: "SSD", - SubPath: "subdir", - }, - ShmVolume: util.False(), - Patroni: Patroni{ - InitDB: map[string]string{ - "encoding": "UTF8", - "locale": "en_US.UTF-8", - "data-checksums": "true", - }, - PgHba: []string{"hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5"}, - TTL: 30, - LoopWait: 10, - RetryTimeout: 10, - MaximumLagOnFailover: 33554432, - Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}}, - }, - Resources: &Resources{ - ResourceRequests: ResourceDescription{CPU: stringToPointer("10m"), Memory: stringToPointer("50Mi")}, - ResourceLimits: ResourceDescription{CPU: stringToPointer("300m"), Memory: stringToPointer("3000Mi")}, - }, - - TeamID: "acid", - AllowedSourceRanges: []string{"127.0.0.1/32"}, - NumberOfInstances: 2, - Users: map[string]UserFlags{"zalando": {"superuser", "createdb"}}, - MaintenanceWindows: []MaintenanceWindow{{ - Everyday: false, - Weekday: time.Monday, - StartTime: mustParseTime("01:00"), - EndTime: mustParseTime("06:00"), - }, { - Everyday: false, - Weekday: time.Saturday, - StartTime: mustParseTime("00:00"), - EndTime: mustParseTime("04:00"), - }, - { - Everyday: true, - Weekday: time.Sunday, - StartTime: mustParseTime("05:00"), - EndTime: mustParseTime("05:15"), - }, - }, - Clone: &CloneDescription{ - ClusterName: "acid-batman", - }, - }, - Error: "", - }, - marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"18","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"pod_priority_class_name":"spilo-pod-priority","volume":{"size":"5Gi","storageClass":"SSD", "subPath": "subdir"},"enableShmVolume":false,"patroni":{"initdb":{"data-checksums":"true","encoding":"UTF8","locale":"en_US.UTF-8"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}},"resources":{"requests":{"cpu":"10m","memory":"50Mi"},"limits":{"cpu":"300m","memory":"3000Mi"}},"teamId":"acid","allowedSourceRanges":["127.0.0.1/32"],"numberOfInstances":2,"users":{"zalando":["superuser","createdb"]},"maintenanceWindows":["Mon:01:00-06:00","Sat:00:00-04:00","05:00-05:15"],"clone":{"cluster":"acid-batman"}},"status":{"PostgresClusterStatus":""}}`), - err: nil}, + err: nil, + }, { about: "example with clone", in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "clone": {"cluster": "team-batman"}}}`), @@ -369,7 +221,8 @@ var unmarshalCluster = []struct { Error: "", }, marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}},"status":{"PostgresClusterStatus":""}}`), - err: nil}, + err: nil, + }, { about: "standby example", in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "standby": {"s3_wal_path": "s3://custom/path/to/bucket/"}}}`), @@ -390,7 +243,8 @@ var unmarshalCluster = []struct { Error: "", }, marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"standby":{"s3_wal_path":"s3://custom/path/to/bucket/"}},"status":{"PostgresClusterStatus":""}}`), - err: nil}, + err: nil, + }, { about: "expect error on malformatted JSON", in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1"`), 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 83c4763ac..4601cf668 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -848,16 +848,6 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { *out = new(int32) **out = **in } - if in.UseLoadBalancer != nil { - in, out := &in.UseLoadBalancer, &out.UseLoadBalancer - *out = new(bool) - **out = **in - } - if in.ReplicaLoadBalancer != nil { - in, out := &in.ReplicaLoadBalancer, &out.ReplicaLoadBalancer - *out = new(bool) - **out = **in - } if in.AllowedSourceRanges != nil { in, out := &in.AllowedSourceRanges, &out.AllowedSourceRanges *out = make([]string, len(*in)) @@ -1026,13 +1016,6 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.InitContainersOld != nil { - in, out := &in.InitContainersOld, &out.InitContainersOld - *out = make([]corev1.Container, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } return } diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index eed4ee933..06c76b279 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -1084,7 +1084,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { // Patroni service and endpoints / config maps if err := c.syncPatroniResources(); err != nil { - c.logger.Errorf("could not sync services: %v", err) + c.logger.Errorf("could not sync Patroni resources: %v", err) updateFailed = true } diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 2b05e2c4a..09cffea36 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -1340,28 +1340,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef } } - // backward compatible check for InitContainers - if spec.InitContainersOld != nil { - msg := "manifest parameter init_containers is deprecated." - if spec.InitContainers == nil { - c.logger.Warningf("%s Consider using initContainers instead.", msg) - spec.InitContainers = spec.InitContainersOld - } else { - c.logger.Warningf("%s Only value from initContainers is used", msg) - } - } - - // backward compatible check for PodPriorityClassName - if spec.PodPriorityClassNameOld != "" { - msg := "manifest parameter pod_priority_class_name is deprecated." - if spec.PodPriorityClassName == "" { - c.logger.Warningf("%s Consider using podPriorityClassName instead.", msg) - spec.PodPriorityClassName = spec.PodPriorityClassNameOld - } else { - c.logger.Warningf("%s Only value from podPriorityClassName is used", msg) - } - } - spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, &c.OpConfig, c.logger) if err != nil { return nil, fmt.Errorf("could not generate Spilo JSON configuration: %v", err) diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index 942ff9951..e6f04fe6b 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -213,22 +213,10 @@ func (c *Controller) processEvent(event ClusterEvent, isInInitialList bool) { } lg.Debugf("observed cluster status %s, running sync scan to repair the cluster", lastOperationStatus) event.EventType = EventSync - } - - if event.EventType == EventAdd || event.EventType == EventUpdate || event.EventType == EventSync { - // handle deprecated parameters by possibly assigning their values to the new ones. - if event.OldSpec != nil { - c.mergeDeprecatedPostgreSQLSpecParameters(&event.OldSpec.Spec) - } - if event.NewSpec != nil { - c.warnOnDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec) - c.mergeDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec) - } - + } else if event.EventType != EventDelete { if err = c.submitRBACCredentials(event); err != nil { c.logger.Warnf("pods and/or Patroni may misfunction due to the lack of permissions: %v", err) } - } switch event.EventType { @@ -397,45 +385,6 @@ func (c *Controller) processClusterEventsQueue(idx int, stopCh <-chan struct{}, } } -func (c *Controller) warnOnDeprecatedPostgreSQLSpecParameters(spec *acidv1.PostgresSpec) { - deprecate := func(deprecated, replacement string) { - c.logger.Warningf("parameter %q is deprecated. Consider setting %q instead", deprecated, replacement) - } - - if spec.UseLoadBalancer != nil { - deprecate("useLoadBalancer", "enableMasterLoadBalancer") - } - if spec.ReplicaLoadBalancer != nil { - deprecate("replicaLoadBalancer", "enableReplicaLoadBalancer") - } - - 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") - } -} - -// mergeDeprecatedPostgreSQLSpecParameters modifies the spec passed to the cluster by setting current parameter -// values from the obsolete ones. Note: while the spec that is modified is a copy made in queueClusterEvent, it is -// still a shallow copy, so be extra careful not to modify values pointer fields point to, but copy them instead. -func (c *Controller) mergeDeprecatedPostgreSQLSpecParameters(spec *acidv1.PostgresSpec) *acidv1.PostgresSpec { - if (spec.UseLoadBalancer != nil || spec.ReplicaLoadBalancer != nil) && - (spec.EnableReplicaLoadBalancer == nil && spec.EnableMasterLoadBalancer == nil) { - if spec.UseLoadBalancer != nil { - spec.EnableMasterLoadBalancer = new(bool) - *spec.EnableMasterLoadBalancer = *spec.UseLoadBalancer - } - if spec.ReplicaLoadBalancer != nil { - spec.EnableReplicaLoadBalancer = new(bool) - *spec.EnableReplicaLoadBalancer = *spec.ReplicaLoadBalancer - } - } - spec.ReplicaLoadBalancer = nil - spec.UseLoadBalancer = nil - - return spec -} - func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1.Postgresql, eventType EventType) { var ( uid types.UID diff --git a/pkg/controller/postgresql_test.go b/pkg/controller/postgresql_test.go index 71d23a264..e6645e188 100644 --- a/pkg/controller/postgresql_test.go +++ b/pkg/controller/postgresql_test.go @@ -64,35 +64,6 @@ func TestControllerOwnershipOnPostgresql(t *testing.T) { } } -func TestMergeDeprecatedPostgreSQLSpecParameters(t *testing.T) { - tests := []struct { - name string - in *acidv1.PostgresSpec - out *acidv1.PostgresSpec - error string - }{ - { - "Check that old parameters propagate values to the new ones", - &acidv1.PostgresSpec{UseLoadBalancer: &True, ReplicaLoadBalancer: &True}, - &acidv1.PostgresSpec{UseLoadBalancer: nil, ReplicaLoadBalancer: nil, - EnableMasterLoadBalancer: &True, EnableReplicaLoadBalancer: &True}, - "New parameters should be set from the values of old ones", - }, - { - "Check that new parameters are not set when both old and new ones are present", - &acidv1.PostgresSpec{UseLoadBalancer: &True, EnableMasterLoadBalancer: &False}, - &acidv1.PostgresSpec{UseLoadBalancer: nil, EnableMasterLoadBalancer: &False}, - "New parameters should remain unchanged when both old and new are present", - }, - } - for _, tt := range tests { - result := postgresqlTestController.mergeDeprecatedPostgreSQLSpecParameters(tt.in) - if !reflect.DeepEqual(result, tt.out) { - t.Errorf("%s: %v", tt.name, tt.error) - } - } -} - func TestMeetsClusterDeleteAnnotations(t *testing.T) { // set delete annotations in configuration postgresqlTestController.opConfig.DeleteAnnotationDateKey = "delete-date" From 85d7aebab2f74ccb62ae691f536eb4fd1c681f00 Mon Sep 17 00:00:00 2001 From: Tiago Condeixa Date: Wed, 15 Jul 2026 11:24:20 +0200 Subject: [PATCH 42/70] adjust makefile and scripts to also run on macos (#3130) --- Makefile | 9 ++++++--- hack/adjust_postgresql_crd.sh | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index d85b74ed3..e6bf4d88f 100644 --- a/Makefile +++ b/Makefile @@ -29,16 +29,19 @@ PKG := `go list ./... | grep -v /vendor/` ifeq ($(DEBUG),1) DOCKERFILE = DebugDockerfile - DEBUG_POSTFIX := -debug-$(shell date hhmmss) + DEBUG_POSTFIX := -debug-$(shell date +"%H%M%S") BUILD_FLAGS += -gcflags "-N -l" else DOCKERFILE = Dockerfile endif + ifeq ($(FRESH),1) DEBUG_FRESH=$(shell date +"%H-%M-%S") endif +SED := $(shell command -v gsed 2>/dev/null || command -v sed) + ifdef CDP_PULL_REQUEST_NUMBER CDP_TAG := -${CDP_BUILD_VERSION} endif @@ -69,8 +72,8 @@ $(GENERATED_CRDS): $(GENERATED) go tool controller-gen crd:crdVersions=v1,allowDangerousTypes=true paths=./pkg/apis/acid.zalan.do/... output:crd:dir=manifests @mv manifests/acid.zalan.do_postgresqls.yaml manifests/postgresql.crd.yaml @# hack to use lowercase kind and listKind - @sed -i -e 's/kind: Postgresql/kind: postgresql/' manifests/postgresql.crd.yaml - @sed -i -e 's/listKind: PostgresqlList/listKind: postgresqlList/' manifests/postgresql.crd.yaml + @$(SED) -i -e 's/kind: Postgresql/kind: postgresql/' manifests/postgresql.crd.yaml + @$(SED) -i -e 's/listKind: PostgresqlList/listKind: postgresqlList/' manifests/postgresql.crd.yaml @hack/adjust_postgresql_crd.sh @mv manifests/acid.zalan.do_operatorconfigurations.yaml manifests/operatorconfiguration.crd.yaml @mv manifests/acid.zalan.do_postgresteams.yaml manifests/postgresteam.crd.yaml diff --git a/hack/adjust_postgresql_crd.sh b/hack/adjust_postgresql_crd.sh index d06b74a2d..29d2788a7 100755 --- a/hack/adjust_postgresql_crd.sh +++ b/hack/adjust_postgresql_crd.sh @@ -13,12 +13,14 @@ file="${1:-"manifests/postgresql.crd.yaml"}" -sed -i '/^[[:space:]]*standby:$/{ +SED=$(command -v gsed 2>/dev/null || command -v sed) + +$SED -i '/^[[:space:]]*standby:$/{ # Capture the indentation s/^\([[:space:]]*\)standby:$/\1standby:\n\1 anyOf:\n\1 - required:\n\1 - s3_wal_path\n\1 - required:\n\1 - gs_wal_path\n\1 - required:\n\1 - standby_host\n\1 not:\n\1 required:\n\1 - s3_wal_path\n\1 - gs_wal_path/ }' "$file" -sed -i '/^[[:space:]]*maintenanceWindows:$/{ +$SED -i '/^[[:space:]]*maintenanceWindows:$/{ # Capture the indentation s/^\([[:space:]]*\)maintenanceWindows:$/\1maintenanceWindows:\n\1 items:\n\1 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))\\ *$'\''\n\1 type: string/ }' "$file" From 7578f9d2c0a0c5f70ed8dc374ce4693ffaf20116 Mon Sep 17 00:00:00 2001 From: Tiago Condeixa Date: Wed, 15 Jul 2026 18:19:10 +0200 Subject: [PATCH 43/70] feat (operator): add support for IRSA (aws resources access) (#3128) * fix data to POSIX and sed working on macos * add ServiceAccountGetter to the newFakeK8sAnnotationsClient for unit tests * try to update the service account * use irsa_role_arn since we need the full arn, and remove enable_irsa * move sa sync code to existing sync.go file to be all together * change all Irsa to IRSA to follow go idiomatic that capitalize initialisms or acronyms * using Update instead of Patch for the service account syn * document the new option and add the key in the values/configs * add the new option to the administrator docs * trying to increase the timeout for the flaky test after sync --------- Co-authored-by: Felix Kunde --- .../crds/operatorconfigurations.yaml | 2 + charts/postgres-operator/values.yaml | 2 + docs/administrator.md | 28 ++++++++ docs/reference/operator_parameters.md | 9 +++ e2e/tests/test_e2e.py | 2 +- manifests/configmap.yaml | 1 + manifests/operatorconfiguration.crd.yaml | 2 + ...gresql-operator-default-configuration.yaml | 1 + .../v1/operator_configuration_type.go | 1 + .../v1/operatorconfiguration.crd.yaml | 2 + pkg/cluster/cluster.go | 6 ++ pkg/cluster/sync.go | 68 +++++++++++++++++++ pkg/cluster/util_test.go | 1 + pkg/controller/controller.go | 6 ++ pkg/controller/operator_config.go | 1 + pkg/util/config/config.go | 1 + pkg/util/constants/annotations.go | 1 + 17 files changed, 133 insertions(+), 1 deletion(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 9cd6b27b7..ebe4e7089 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -73,6 +73,8 @@ spec: type: integer gcp_credentials: type: string + irsa_role_arn: + type: string kube_iam_role: type: string log_s3_bucket: diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 3a2194ef0..4ea5b6319 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -360,6 +360,8 @@ configAwsOrGcp: # AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods # kube_iam_role: "" + # Full ARN for IRSA (IAM Roles for Service Accounts) on EKS + # irsa_role_arn: "" # S3 bucket to use for shipping postgres daily logs # log_s3_bucket: "" diff --git a/docs/administrator.md b/docs/administrator.md index ec0bc28dc..49718d1ea 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -1094,6 +1094,32 @@ configuration: wal_s3_bucket: your-backup-path ``` +Alternatively, if your cluster uses EKS with OIDC, you can use +[IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) +(IAM Roles for Service Accounts) instead of kube2iam. Set `irsa_role_arn` to +the full ARN of the IAM role: + +**OperatorConfiguration** + +```yaml +apiVersion: "acid.zalan.do/v1" +kind: OperatorConfiguration +metadata: + name: postgresql-operator-configuration +configuration: + aws_or_gcp: + aws_region: eu-central-1 + irsa_role_arn: arn:aws:iam::123456789012:role/postgres-pod-role + wal_s3_bucket: your-backup-path +``` + +When `irsa_role_arn` is set the operator annotates the pod service account with +`eks.amazonaws.com/role-arn` on every reconcile. The EKS OIDC webhook then +injects an AWS web identity token into each pod, which takes precedence over +the EC2 metadata credentials used by kube2iam. Both `kube_iam_role` and +`irsa_role_arn` can coexist during a migration — existing pods retain the +kube2iam annotation until they are rotated, at which point only IRSA is used. + The referenced IAM role should contain the following privileges to make sure Postgres can send compressed WAL files to the given S3 bucket: @@ -1204,6 +1230,7 @@ aws_or_gcp: # additional_secret_mount_path: "" # aws_region: eu-central-1 # kube_iam_role: "" + # irsa_role_arn: "" # log_s3_bucket: "" # wal_s3_bucket: "" wal_gs_bucket: "postgres-backups-bucket-28302F2" # name of bucket on where to save the WAL-E logs @@ -1253,6 +1280,7 @@ aws_or_gcp: additional_secret_mount_path: "/var/secrets/google" # or where ever you want to mount the file # aws_region: eu-central-1 # kube_iam_role: "" + # irsa_role_arn: "" # log_s3_bucket: "" # wal_s3_bucket: "" wal_gs_bucket: "postgres-backups-bucket-28302F2" # name of bucket on where to save the WAL-E logs diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index dd7487352..cf1d18c14 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -798,6 +798,15 @@ yet officially supported. [kube2iam](https://github.com/jtblin/kube2iam) project on AWS. The default is empty. +* **irsa_role_arn** + Full AWS IAM role ARN to supply in the `eks.amazonaws.com/role-arn` annotation + of the Postgres pod service account, enabling + [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) + (IAM Roles for Service Accounts) on EKS. When set, the operator annotates the + pod service account on every sync so that the EKS OIDC webhook can inject AWS + credentials directly into pods. Must be a full ARN, e.g. + `arn:aws:iam::123456789012:role/my-postgres-role`. The default is empty. + * **aws_region** AWS region used to store EBS volumes. The default is `eu-central-1`. Note, this option is not meant for specifying the AWS region for backups and diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 7ce80a6c9..8b64185e9 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -1430,7 +1430,7 @@ def test_resource_generation(self): k8s.api.custom_objects_api.patch_namespaced_custom_object( "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_resources) self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, - "Operator does not get in sync") + "Operator does not get in sync", retries=120) # wait for switched over k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label) diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 1c663c757..c7f4d2655 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -79,6 +79,7 @@ data: # inherited_annotations: owned-by # inherited_labels: application,environment # kube_iam_role: "" + # irsa_role_arn: "" kubernetes_use_configmaps: "false" # log_s3_bucket: "" # logical_backup_azure_storage_account_name: "" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 9cd6b27b7..ebe4e7089 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -73,6 +73,8 @@ spec: type: integer gcp_credentials: type: string + irsa_role_arn: + type: string kube_iam_role: type: string log_s3_bucket: diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index daa7f7b13..e25a7b339 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -171,6 +171,7 @@ configuration: # enable_ebs_gp3_migration_max_size: 1000 # gcp_credentials: "" # kube_iam_role: "" + # irsa_role_arn: "" # log_s3_bucket: "" # wal_az_storage_account: "" # wal_gs_bucket: "" 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 b84bce03a..046dd0761 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -252,6 +252,7 @@ type AWSGCPConfiguration struct { WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"` LogS3Bucket string `json:"log_s3_bucket,omitempty"` KubeIAMRole string `json:"kube_iam_role,omitempty"` + IRSARoleARN string `json:"irsa_role_arn,omitempty"` AdditionalSecretMount string `json:"additional_secret_mount,omitempty"` AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"` EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration,omitempty"` diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 9cd6b27b7..ebe4e7089 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -73,6 +73,8 @@ spec: type: integer gcp_credentials: type: string + irsa_role_arn: + type: string kube_iam_role: type: string log_s3_bucket: diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 06c76b279..836bd086e 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -1140,6 +1140,12 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { c.logger.Infof("Storage resize is disabled (storage_resize_mode is off). Skipping volume size sync.") } + // Pod service account (IRSA annotation sync) + if err := c.syncPodServiceAccount(); err != nil { + c.logger.Errorf("could not sync pod service account: %v", err) + updateFailed = true + } + // Statefulset func() { if err := c.syncStatefulSet(); err != nil { diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index af3f786df..5ada7ea9a 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -107,6 +107,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { } } + if err = c.syncPodServiceAccount(); err != nil { + c.logger.Errorf("could not sync pod service account: %v", err) + } + if err = c.syncStatefulSet(); err != nil { if !k8sutil.ResourceAlreadyExists(err) { err = fmt.Errorf("could not sync statefulsets: %v", err) @@ -630,6 +634,10 @@ func (c *Cluster) syncStatefulSet() error { if !cmp.rollingUpdate { updatedPodAnnotations := map[string]*string{} for _, anno := range cmp.deletedPodAnnotations { + // during IRSA migration let kube2iam annotation drain naturally via pod rotation + if c.OpConfig.IRSARoleARN != "" && anno == constants.KubeIAmAnnotation { + continue + } updatedPodAnnotations[anno] = nil } for anno, val := range desiredSts.Spec.Template.Annotations { @@ -1803,6 +1811,7 @@ func (c *Cluster) syncLogicalBackupJob() error { // no existing logical backup job, create new one c.logger.Info("could not find the cluster's logical backup job") + if err = c.createLogicalBackupJob(); err == nil { c.logger.Infof("created missing logical backup job %s", jobName) } else { @@ -1817,3 +1826,62 @@ func (c *Cluster) syncLogicalBackupJob() error { return nil } + +func (c *Cluster) syncPodServiceAccount() error { + sa, err := c.KubeClient.ServiceAccounts(c.Namespace).Get(context.TODO(), c.OpConfig.PodServiceAccountName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("could not get pod service account %q: %v", c.OpConfig.PodServiceAccountName, err) + } + + changed := false + + if c.OpConfig.IRSARoleARN != "" { + if val, ok := sa.Annotations[constants.IRSAAnnotation]; !ok || val != c.OpConfig.IRSARoleARN { + if sa.Annotations == nil { + sa.Annotations = make(map[string]string) + } + sa.Annotations[constants.IRSAAnnotation] = c.OpConfig.IRSARoleARN + changed = true + } + } else { + if _, ok := sa.Annotations[constants.IRSAAnnotation]; ok { + delete(sa.Annotations, constants.IRSAAnnotation) + changed = true + } + } + + if changed { + if _, err = c.KubeClient.ServiceAccounts(c.Namespace).Update(context.TODO(), sa, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("could not update pod service account %q: %v", sa.Name, err) + } + c.logger.Infof("synced annotations on pod service account %q", sa.Name) + } + + if c.OpConfig.IRSARoleARN != "" { + c.logIRSAMigrationProgress() + } + + return nil +} + +func (c *Cluster) logIRSAMigrationProgress() { + pods, err := c.listPods() + if err != nil { + c.logger.Warnf("IRSA migration: could not list pods: %v", err) + return + } + + total := len(pods) + remaining := 0 + for _, pod := range pods { + if _, ok := pod.Annotations[constants.KubeIAmAnnotation]; ok { + remaining++ + } + } + + if remaining > 0 { + c.logger.Infof("IRSA migration in progress: %d/%d pods still carry kube2iam annotation, will be removed on next rotation", remaining, total) + } else { + c.logger.Infof("IRSA migration complete: all %d pods have rotated, kube2iam annotation fully drained", total) + } +} diff --git a/pkg/cluster/util_test.go b/pkg/cluster/util_test.go index d97668ee8..dfae6237a 100644 --- a/pkg/cluster/util_test.go +++ b/pkg/cluster/util_test.go @@ -53,6 +53,7 @@ func newFakeK8sAnnotationsClient() (k8sutil.KubernetesClient, *k8sFake.Clientset EndpointsGetter: clientSet.CoreV1(), ConfigMapsGetter: clientSet.CoreV1(), PodsGetter: clientSet.CoreV1(), + ServiceAccountsGetter: clientSet.CoreV1(), DeploymentsGetter: clientSet.AppsV1(), CronJobsGetter: clientSet.BatchV1(), }, clientSet diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 879e44ad4..63434efed 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -247,6 +247,12 @@ func (c *Controller) initPodServiceAccount() { c.PodServiceAccount.Name = c.opConfig.PodServiceAccountName } c.PodServiceAccount.Namespace = "" + if c.opConfig.IRSARoleARN != "" { + if c.PodServiceAccount.Annotations == nil { + c.PodServiceAccount.Annotations = make(map[string]string) + } + c.PodServiceAccount.Annotations[constants.IRSAAnnotation] = c.opConfig.IRSARoleARN + } } // actual service accounts are deployed at the time of Postgres/Spilo cluster creation diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index b3765e387..667384e30 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -175,6 +175,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.AWSRegion = fromCRD.AWSGCP.AWSRegion result.LogS3Bucket = fromCRD.AWSGCP.LogS3Bucket result.KubeIAMRole = fromCRD.AWSGCP.KubeIAMRole + result.IRSARoleARN = fromCRD.AWSGCP.IRSARoleARN result.WALGSBucket = fromCRD.AWSGCP.WALGSBucket result.GCPCredentials = fromCRD.AWSGCP.GCPCredentials result.WALAZStorageAccount = fromCRD.AWSGCP.WALAZStorageAccount diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 14ae537cc..95324a747 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -194,6 +194,7 @@ type Config struct { WALES3Bucket string `name:"wal_s3_bucket"` LogS3Bucket string `name:"log_s3_bucket"` KubeIAMRole string `name:"kube_iam_role"` + IRSARoleARN string `name:"irsa_role_arn"` WALGSBucket string `name:"wal_gs_bucket"` GCPCredentials string `name:"gcp_credentials"` WALAZStorageAccount string `name:"wal_az_storage_account"` diff --git a/pkg/util/constants/annotations.go b/pkg/util/constants/annotations.go index 0330ddcb8..d5d925e2e 100644 --- a/pkg/util/constants/annotations.go +++ b/pkg/util/constants/annotations.go @@ -4,6 +4,7 @@ package constants const ( ZalandoDNSNameAnnotation = "external-dns.alpha.kubernetes.io/hostname" KubeIAmAnnotation = "iam.amazonaws.com/role" + IRSAAnnotation = "eks.amazonaws.com/role-arn" VolumeStorateProvisionerAnnotation = "pv.kubernetes.io/provisioned-by" PostgresqlControllerAnnotationKey = "acid.zalan.do/controller" ) From 5c6dcc65e2579f30831d8d4199619755c63e4e09 Mon Sep 17 00:00:00 2001 From: annielzy <148128409+annielzy@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:56:48 -0700 Subject: [PATCH 44/70] Preserve sync quorum in primary PDB without master selector (#3119) * Use minAvalable=SynchronousNodeCount + 1 in primary PDB when master selector is disabled --------- Co-authored-by: Felix Kunde Co-authored-by: Ida Novindasari --- pkg/cluster/k8sres.go | 6 ++++++ pkg/cluster/k8sres_test.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 09cffea36..505977ed9 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -2278,6 +2278,12 @@ func (c *Cluster) generatePrimaryPodDisruptionBudget() *policyv1.PodDisruptionBu labels[c.OpConfig.PodRoleLabel] = string(Master) } + // When master selector is disabled and synchronous_mode_strict is on, require + // master + synchronous_node_count (default 1) healthy pods for write quorum. + if pdbMasterLabelSelector != nil && !*pdbMasterLabelSelector && minAvailable.IntVal > 0 && c.Spec.SynchronousModeStrict { + minAvailable = intstr.FromInt32(int32(c.Spec.SynchronousNodeCount + 1)) + } + return &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: c.PrimaryPodDisruptionBudgetName(), diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index d51efb085..cb2655777 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -2691,13 +2691,13 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { k8sutil.KubernetesClient{}, acidv1.Postgresql{ ObjectMeta: metav1.ObjectMeta{Name: "myapp-database", Namespace: "myapp"}, - Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3}}, + Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3, Patroni: acidv1.Patroni{SynchronousModeStrict: true, SynchronousNodeCount: 1}}}, logger, eventRecorder), check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ testPodDisruptionBudgetOwnerReference, hasName("postgres-myapp-database-pdb"), - hasMinAvailable(1), + hasMinAvailable(2), testLabelsAndSelectors(true), }, }, From 118c1146bf949e694a8de416bc17073e6aef8c4c Mon Sep 17 00:00:00 2001 From: Pierre Ozoux Date: Fri, 17 Jul 2026 11:57:30 +0200 Subject: [PATCH 45/70] docs: correct kubectl commands (#3129) Co-authored-by: Felix Kunde Co-authored-by: Ida Novindasari --- docs/reference/operator_parameters.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index cf1d18c14..02c6db8d0 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -42,14 +42,14 @@ and change it. To test the CRD-based configuration locally, use the following -```bash - kubectl create -f manifests/operatorconfiguration.crd.yaml # registers the CRD - kubectl create -f manifests/postgresql-operator-default-configuration.yaml +``` +kubectl create -f manifests/operatorconfiguration.crd.yaml # registers the CRD +kubectl create -f manifests/postgresql-operator-default-configuration.yaml - kubectl create -f manifests/operator-service-account-rbac.yaml - kubectl create -f manifests/postgres-operator.yaml # set the env var as mentioned above +kubectl create -f manifests/operator-service-account-rbac.yaml +kubectl create -f manifests/postgres-operator.yaml # set the env var as mentioned above - kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml +kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml ``` The CRD-based configuration is more powerful than the one based on ConfigMaps From f2741ef7418decb2dd1e4483b1188feb5dfd923f Mon Sep 17 00:00:00 2001 From: Tiago Condeixa Date: Fri, 17 Jul 2026 15:36:00 +0200 Subject: [PATCH 46/70] we need to add update permission for sa to periodically sync (#3131) --- charts/postgres-operator/templates/clusterrole.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/postgres-operator/templates/clusterrole.yaml b/charts/postgres-operator/templates/clusterrole.yaml index ad3b46064..6846a4bdf 100644 --- a/charts/postgres-operator/templates/clusterrole.yaml +++ b/charts/postgres-operator/templates/clusterrole.yaml @@ -234,6 +234,7 @@ rules: verbs: - get - create + - update # to create role bindings to the postgres-pod service account - apiGroups: - rbac.authorization.k8s.io From 9e63ca2dfddf5c4a81382aa94b1bfe9b50a4c7bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:53:18 +0200 Subject: [PATCH 47/70] Bump flask from 3.0.3 to 3.1.3 in /ui (#3048) Bumps [flask](https://github.com/pallets/flask) from 3.0.3 to 3.1.3. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/3.0.3...3.1.3) --- updated-dependencies: - dependency-name: flask dependency-version: 3.1.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/requirements.txt b/ui/requirements.txt index ace18641d..4a8874dfe 100644 --- a/ui/requirements.txt +++ b/ui/requirements.txt @@ -2,7 +2,7 @@ backoff==2.2.1 boto3==1.34.110 boto==2.49.0 click==8.1.7 -Flask==3.0.3 +Flask==3.1.3 furl==2.1.3 gevent==24.2.1 jq==1.7.0 From 8ab2b9b3d82b6eb822579935d4ab5e187a3dfa55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:38:14 +0200 Subject: [PATCH 48/70] Bump golang.org/x/crypto from 0.51.0 to 0.52.0 (#3127) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.51.0 to 0.52.0. - [Commits](https://github.com/golang/crypto/compare/v0.51.0...v0.52.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.52.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 37c2d2b65..8e8d27f78 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/r3labs/diff v1.1.0 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.52.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.36.1 k8s.io/apiextensions-apiserver v0.36.1 diff --git a/go.sum b/go.sum index c4927d42e..87b5376cc 100644 --- a/go.sum +++ b/go.sum @@ -148,8 +148,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= From ef2efe3aadd7c874c4ab787b060bdca65c105eef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:38:27 +0200 Subject: [PATCH 49/70] Bump requests from 2.32.4 to 2.33.0 in /ui (#3060) Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.33.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.33.0) --- updated-dependencies: - dependency-name: requests dependency-version: 2.33.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Felix Kunde --- ui/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/requirements.txt b/ui/requirements.txt index 4a8874dfe..72f6597b0 100644 --- a/ui/requirements.txt +++ b/ui/requirements.txt @@ -9,6 +9,6 @@ jq==1.7.0 json_delta>=2.0.2 kubernetes==11.0.0 python-json-logger==2.0.7 -requests==2.32.4 +requests==2.33.0 stups-tokens>=1.1.19 werkzeug==3.1.6 From 46f4315bf18e9b0e163b21f6a49b4e2a8135c91a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:48:51 +0200 Subject: [PATCH 50/70] Bump js-yaml from 4.2.0 to 4.3.0 in /ui/app (#3133) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/package.json b/ui/app/package.json index 7fa1ef373..c5f1e61df 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -38,7 +38,7 @@ "brfs": "^2.0.2", "dedent-js": "1.0.1", "eslint": "^8.32.0", - "js-yaml": "4.2.0", + "js-yaml": "4.3.0", "pug": "^3.0.2", "rimraf": "^4.1.2", "riot": "^3.13.2", From 1b460310a2e1f464aaf27afb4e62a2a8d8fd60e1 Mon Sep 17 00:00:00 2001 From: Tiago Condeixa Date: Wed, 22 Jul 2026 16:40:06 +0200 Subject: [PATCH 51/70] add env var for spilo with irsa (#3135) --- pkg/cluster/k8sres.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 505977ed9..49aeab8a0 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -2214,6 +2214,11 @@ func (c *Cluster) generateCloneEnvironment(description *acidv1.CloneDescription) } } + if c.OpConfig.IRSARoleARN != "" { + result = append(result, v1.EnvVar{Name: "CLONE_AWS_ROLE_ARN", Value: c.OpConfig.IRSARoleARN}) + result = append(result, v1.EnvVar{Name: "CLONE_AWS_WEB_IDENTITY_TOKEN_FILE", Value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}) + } + return result } @@ -2259,6 +2264,11 @@ func (c *Cluster) generateStandbyEnvironment(description *acidv1.StandbyDescript result = append(result, v1.EnvVar{Name: "STANDBY_WAL_BUCKET_SCOPE_PREFIX", Value: ""}) } + if c.OpConfig.IRSARoleARN != "" { + result = append(result, v1.EnvVar{Name: "STANDBY_AWS_ROLE_ARN", Value: c.OpConfig.IRSARoleARN}) + result = append(result, v1.EnvVar{Name: "STANDBY_AWS_WEB_IDENTITY_TOKEN_FILE", Value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}) + } + return result } From ca9513c8319a6324974e7c931e1a47d6c8ef2237 Mon Sep 17 00:00:00 2001 From: annielzy <148128409+annielzy@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:21:31 -0700 Subject: [PATCH 52/70] Add envFrom support to Postgresql spec (#3118) * add envFrom support * generate files --------- Co-authored-by: Felix Kunde --- .../postgres-operator/crds/postgresqls.yaml | 45 +++++++++++++++ manifests/postgresql.crd.yaml | 46 +++++++++++++++ pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 46 +++++++++++++++ pkg/apis/acid.zalan.do/v1/postgresql_type.go | 1 + .../acid.zalan.do/v1/zz_generated.deepcopy.go | 7 +++ pkg/cluster/cluster_test.go | 57 +++++++++++++++++++ pkg/cluster/k8sres.go | 4 ++ pkg/cluster/k8sres_test.go | 42 ++++++++++++++ 8 files changed, 248 insertions(+) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 94db2d442..4d534c16e 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -454,6 +454,51 @@ spec: - name type: object type: array + envFrom: + items: + description: EnvFromSource represents the source of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each key in + the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic initContainers: items: description: A single application container that you want to run diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 94db2d442..0e3558638 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -454,6 +454,52 @@ spec: - name type: object type: array + envFrom: + items: + description: EnvFromSource represents the source of a set of ConfigMaps + or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array initContainers: items: description: A single application container that you want to run diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index 94db2d442..0e3558638 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -454,6 +454,52 @@ spec: - name type: object type: array + envFrom: + items: + description: EnvFromSource represents the source of a set of ConfigMaps + or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array initContainers: items: description: A single application container that you want to run diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 52d51cd41..a03bb13cc 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -124,6 +124,7 @@ type PostgresSpec struct { AdditionalVolumes []AdditionalVolume `json:"additionalVolumes,omitempty"` Streams []Stream `json:"streams,omitempty"` Env []v1.EnvVar `json:"env,omitempty"` + EnvFrom []v1.EnvFromSource `json:"envFrom,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 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 4601cf668..60003adfa 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -1016,6 +1016,13 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = make([]corev1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index b2c6bee68..2db1223b4 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -229,6 +229,63 @@ func TestStatefulSetUpdateWithEnv(t *testing.T) { } } +func TestStatefulSetUpdateWithEnvFrom(t *testing.T) { + oldSpec := &acidv1.PostgresSpec{ + TeamID: "myapp", NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + Volume: acidv1.Volume{ + Size: "1G", + }, + } + oldSS, err := cl.generateStatefulSet(oldSpec) + if err != nil { + t.Errorf("in %s no StatefulSet created %v", t.Name(), err) + } + + newSpec := oldSpec.DeepCopy() + newSS, err := cl.generateStatefulSet(newSpec) + if err != nil { + t.Errorf("in %s no StatefulSet created %v", t.Name(), err) + } + + if !reflect.DeepEqual(oldSS, newSS) { + t.Errorf("in %s StatefulSet's must be equal", t.Name()) + } + + newSpec.EnvFrom = []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + } + newSS, err = cl.generateStatefulSet(newSpec) + if err != nil { + t.Errorf("in %s no StatefulSet created %v", t.Name(), err) + } + + if reflect.DeepEqual(oldSS, newSS) { + t.Errorf("in %s StatefulSet's must be not equal", t.Name()) + } + + postgresContainer := newSS.Spec.Template.Spec.Containers[0] + if !reflect.DeepEqual(postgresContainer.EnvFrom, newSpec.EnvFrom) { + t.Errorf("expected envFrom %v, got %v", newSpec.EnvFrom, postgresContainer.EnvFrom) + } +} + func TestInitRobotUsers(t *testing.T) { tests := []struct { testCase string diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 49aeab8a0..4814a5546 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -697,6 +697,7 @@ func generateContainer( dockerImage *string, resourceRequirements *v1.ResourceRequirements, envVars []v1.EnvVar, + envFrom []v1.EnvFromSource, volumeMounts []v1.VolumeMount, privilegedMode bool, privilegeEscalationMode *bool, @@ -723,6 +724,7 @@ func generateContainer( }, VolumeMounts: volumeMounts, Env: envVars, + EnvFrom: envFrom, SecurityContext: &v1.SecurityContext{ AllowPrivilegeEscalation: privilegeEscalationMode, Privileged: &privilegedMode, @@ -1385,6 +1387,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef &effectiveDockerImage, resourceRequirements, spiloEnvVars, + spec.EnvFrom, volumeMounts, c.OpConfig.Resources.SpiloPrivileged, c.OpConfig.Resources.SpiloAllowPrivilegeEscalation, @@ -2385,6 +2388,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { &c.OpConfig.LogicalBackup.LogicalBackupDockerImage, resourceRequirements, envVars, + nil, []v1.VolumeMount{}, c.OpConfig.SpiloPrivileged, // use same value as for normal DB pods c.OpConfig.SpiloAllowPrivilegeEscalation, diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index cb2655777..c9ebc020f 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -4488,6 +4488,48 @@ func TestGenerateCapabilities(t *testing.T) { } } +func TestGenerateContainerWithEnvFrom(t *testing.T) { + dockerImage := "test-image" + resourceRequirements := &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("100Mi"), + }, + } + envVars := []v1.EnvVar{{Name: "TEST_VAR", Value: "test-value"}} + envFrom := []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "test-configmap"}, + }, + }, + { + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "test-secret"}, + }, + }, + } + + container := generateContainer( + constants.PostgresContainerName, + &dockerImage, + resourceRequirements, + envVars, + envFrom, + []v1.VolumeMount{}, + false, + util.False(), + nil, + ) + + if !reflect.DeepEqual(container.Env, envVars) { + t.Errorf("expected env %v, got %v", envVars, container.Env) + } + if !reflect.DeepEqual(container.EnvFrom, envFrom) { + t.Errorf("expected envFrom %v, got %v", envFrom, container.EnvFrom) + } +} + func TestTopologySpreadConstraints(t *testing.T) { clusterName := "acid-test-cluster" namespace := "default" From 86d3027e0daffba30189ab1578a93c10db0d14fd Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Thu, 23 Jul 2026 16:24:23 +0200 Subject: [PATCH 53/70] update docker build commands in Makefiles to use buildx (#3137) * switch to docker buildx and update docs * minor update to e2e Dockerfile --- Makefile | 4 ++-- charts/postgres-operator/crds/postgresqls.yaml | 7 ++++--- docker/Dockerfile | 3 +-- docker/build_operator.sh | 1 - docs/administrator.md | 2 +- docs/developer.md | 3 +-- e2e/Dockerfile | 2 +- e2e/Makefile | 2 +- 8 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index e6bf4d88f..950aadd04 100644 --- a/Makefile +++ b/Makefile @@ -101,10 +101,10 @@ docker: $(GENERATED_CRDS) ${DOCKERDIR}/${DOCKERFILE} echo "Version ${VERSION}" echo "CDP tag ${CDP_TAG}" echo "git describe $(shell git describe --tags --always --dirty)" - docker build --rm -t "$(IMAGE_TAG)" -f "${DOCKERDIR}/${DOCKERFILE}" --build-arg VERSION="${VERSION}" --build-arg BASE_IMAGE="${BASE_IMAGE}" . + docker buildx build --load --rm -t "$(IMAGE_TAG)" -f "${DOCKERDIR}/${DOCKERFILE}" --build-arg VERSION="${VERSION}" --build-arg BASE_IMAGE="${BASE_IMAGE}" . pooler: - cd pooler; docker build --rm -t "$(POOLER_TAG)" --build-arg VERSION="${VERSION}" --build-arg BASE_IMAGE="${BASE_IMAGE}" . + cd pooler; docker buildx build --load --rm -t "$(POOLER_TAG)" --build-arg VERSION="${VERSION}" --build-arg BASE_IMAGE="${BASE_IMAGE}" . indocker-race: docker run --rm -v "${GOPATH}":"${GOPATH}" -e GOPATH="${GOPATH}" -e RACE=1 -w ${PWD} golang:1.26.4 bash -c "make linux" diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 4d534c16e..0e3558638 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -457,6 +457,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set of ConfigMaps + or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -476,8 +477,9 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each key in - the ConfigMap. Must be a C_IDENTIFIER. + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. type: string secretRef: description: The Secret to select from @@ -498,7 +500,6 @@ spec: x-kubernetes-map-type: atomic type: object type: array - x-kubernetes-list-type: atomic initContainers: items: description: A single application container that you want to run diff --git a/docker/Dockerfile b/docker/Dockerfile index 4ba9b7630..c6940a7c2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,12 +8,11 @@ ARG TARGETARCH COPY . /go/src/github.com/zalando/postgres-operator WORKDIR /go/src/github.com/zalando/postgres-operator -RUN go clean -cache -modcache RUN go mod vendor \ && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o build/postgres-operator -v -ldflags "-X=main.version=${VERSION}" cmd/main.go ARG BASE_IMAGE -FROM --platform=$TARGETPLATFORM ${BASE_IMAGE} +FROM ${BASE_IMAGE} LABEL maintainer="Team ACID @ Zalando " LABEL org.opencontainers.image.source="https://github.com/zalando/postgres-operator" diff --git a/docker/build_operator.sh b/docker/build_operator.sh index 0b1bf4d82..ab265011b 100644 --- a/docker/build_operator.sh +++ b/docker/build_operator.sh @@ -26,6 +26,5 @@ export PATH="$PATH:$HOME/go/bin" export GOPATH="$HOME/go" mkdir -p build -go clean -cache -modcache go mod vendor CGO_ENABLED=0 go build -o build/postgres-operator -v -ldflags "$OPERATOR_LDFLAGS" cmd/main.go diff --git a/docs/administrator.md b/docs/administrator.md index 49718d1ea..adfee6065 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -1565,7 +1565,7 @@ make docker # build in image in minikube docker env eval $(minikube docker-env) -docker build -t ghcr.io/zalando/postgres-operator-ui:v1.15.1 . +docker buildx build --load -t ghcr.io/zalando/postgres-operator-ui:v1.15.1 . # apply UI manifests next to a running Postgres Operator kubectl apply -f manifests/ diff --git a/docs/developer.md b/docs/developer.md index eb7cb0e9f..2369a7809 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -30,8 +30,7 @@ git clone https://github.com/zalando/postgres-operator.git We use [Go Modules](https://github.com/golang/go/wiki/Modules) for handling dependencies. Run `go mod vendor && go mod tidy` to install them. -Build the operator with the `make docker` command. You may define the TAG variable to assign an explicit tag to your Docker image and the IMAGE to set the image name. By default, the tag is computed with -`git describe --tags --always --dirty` and the image is `ghcr.io/zalando/postgres-operator`. On macos search and replace `sed -i` commands with `sed -i ''` for the make commands to work. +Build the operator with the `make docker` command. You may define the TAG variable to assign an explicit tag to your Docker image and the IMAGE to set the image name. By default, the tag is computed with `git describe --tags --always --dirty` and the image is `ghcr.io/zalando/postgres-operator`. ```bash export TAG=$(git describe --tags --always --dirty) diff --git a/e2e/Dockerfile b/e2e/Dockerfile index 1f4083116..2d30f3218 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.11-slim LABEL maintainer="Team ACID @ Zalando " -ENV TERM xterm-256color +ENV TERM=xterm-256color RUN apt-get -qq -y update \ # https://www.psycopg.org/docs/install.html#psycopg-vs-psycopg-binary diff --git a/e2e/Makefile b/e2e/Makefile index 09ac74986..c0925ec44 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -37,7 +37,7 @@ copy: clean mkdir tls docker: - docker build -t "$(IMAGE):$(TAG)" . + docker buildx build --load --rm -t "$(IMAGE):$(TAG)" . push: docker docker push "$(IMAGE):$(TAG)" From 0ea05f76915f99d65c4c95aa2aec4011ec26ed3a Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Fri, 24 Jul 2026 09:15:26 +0200 Subject: [PATCH 54/70] Fix no space left error during e2e test setup (#3138) * fix space issues for e2e test * pin exact minor release of coveralls --- .github/workflows/run_e2e.yaml | 7 +++++++ .github/workflows/run_tests.yaml | 6 +++--- e2e/run.sh | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run_e2e.yaml b/.github/workflows/run_e2e.yaml index 393109b00..08843ab2b 100644 --- a/.github/workflows/run_e2e.yaml +++ b/.github/workflows/run_e2e.yaml @@ -21,5 +21,12 @@ jobs: run: make codegen - name: Run unit tests run: make test + - name: Pre-pull heavy images directly on Kind nodes + run: | + FOR_NODE=$(kind get nodes --name postgres-operator-e2e-tests 2>/dev/null || true) + for node in $FOR_NODE; do + echo "Pulling Spilo image on node $node..." + docker exec "$node" crictl pull ghcr.io/zalando/spilo-18:4.1-p2 + done - name: Run end-2-end tests run: make e2e diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index c511e8fa1..5f68f6309 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -22,9 +22,9 @@ jobs: - name: Run unit tests run: go test -race -covermode atomic -coverprofile=coverage.out ./... - name: Convert coverage to lcov - uses: jandelgado/gcov2lcov-action@v1.1.1 + uses: jandelgado/gcov2lcov-action@v1.2.0 - name: Coveralls - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2.3.4 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: coverage.lcov + file: coverage.lcov diff --git a/e2e/run.sh b/e2e/run.sh index c5daf81f6..0bcf67989 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -24,6 +24,15 @@ esac echo "Clustername: ${cluster_name}" echo "Kubeconfig path: ${kubeconfig_path}" +# Helper function to pull images directly into kind nodes via crictl (bypasses host disk duplication) +function pull_on_kind_nodes() { + local img="$1" + echo "Pulling ${img} directly on kind nodes..." + for node in $(kind get nodes --name "${cluster_name}"); do + docker exec "$node" crictl pull "${img}" + done +} + function pull_images(){ operator_tag=$(git describe --tags --always --dirty) components=("postgres-operator" "pooler") @@ -66,14 +75,14 @@ function start_kind(){ export KUBECONFIG="${kubeconfig_path}" kind create cluster --name ${cluster_name} --config kind-cluster-postgres-operator-e2e-tests.yaml - echo "Pulling Spilo image for platform ${PLATFORM}" - docker pull --platform ${PLATFORM} "${spilo_image}" - kind load docker-image "${spilo_image}" --name ${cluster_name} + echo "Pulling Spilo image on kind nodes directly..." + pull_on_kind_nodes "${spilo_image}" } function load_operator_images() { echo "Loading operator images" export KUBECONFIG="${kubeconfig_path}" + # For locally built operator images, kind load is still fine since they're light kind load docker-image "${operator_image}" --name ${cluster_name} kind load docker-image "${pooler_image}" --name ${cluster_name} } From bffde0f5e7cc4db91e56bba709a6a02f656568e4 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Fri, 24 Jul 2026 16:39:41 +0200 Subject: [PATCH 55/70] fix type deference when calling ebs modify volume endpoint (#3140) * fix type deference when calling ebs modify volume endpoint * only pass set variable to modify input --- pkg/util/volumes/ebs.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/util/volumes/ebs.go b/pkg/util/volumes/ebs.go index bb7506d93..212ee8ac4 100644 --- a/pkg/util/volumes/ebs.go +++ b/pkg/util/volumes/ebs.go @@ -155,23 +155,32 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { // ModifyVolume Modify EBS volume func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error { - /* first check if the volume is already of a requested size */ var sizeInt32 *int32 var iopsInt32 *int32 var throughputInt32 *int32 + + input := ec2.ModifyVolumeInput{ + VolumeId: &volumeID, + } if newSize != nil { s := int32(*newSize) sizeInt32 = &s + input.Size = sizeInt32 } if iops != nil { i := int32(*iops) iopsInt32 = &i + input.Iops = iopsInt32 } if throughput != nil { t := int32(*throughput) throughputInt32 = &t + input.Throughput = throughputInt32 } - input := ec2.ModifyVolumeInput{Size: sizeInt32, VolumeId: &volumeID, VolumeType: types.VolumeType(*newType), Iops: iopsInt32, Throughput: throughputInt32} + if newType != nil { + input.VolumeType = types.VolumeType(*newType) + } + output, err := r.connection.ModifyVolume(context.TODO(), &input) if err != nil { return fmt.Errorf("could not modify persistent volume: %v", err) From 56a8f1d90b70bb88dd06b7f193be61b822076782 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Mon, 27 Jul 2026 14:02:44 +0200 Subject: [PATCH 56/70] remove gp3 migration flow (#3142) --- .../crds/operatorconfigurations.yaml | 5 - charts/postgres-operator/values.yaml | 6 +- docs/reference/operator_parameters.md | 12 +- docs/user.md | 2 +- manifests/complete-postgres-manifest.yaml | 4 +- manifests/configmap.yaml | 2 - manifests/operatorconfiguration.crd.yaml | 5 - ...gresql-operator-default-configuration.yaml | 2 - .../v1/operator_configuration_type.go | 20 ++- .../v1/operatorconfiguration.crd.yaml | 5 - pkg/cluster/cluster.go | 2 +- pkg/cluster/sync.go | 9 -- pkg/cluster/volumes.go | 65 +-------- pkg/cluster/volumes_test.go | 134 +----------------- pkg/controller/operator_config.go | 2 - pkg/util/config/config.go | 2 - pkg/util/volumes/ebs.go | 14 +- 17 files changed, 31 insertions(+), 260 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index ebe4e7089..5fdf8aad8 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -66,11 +66,6 @@ spec: aws_region: default: eu-central-1 type: string - enable_ebs_gp3_migration: - type: boolean - enable_ebs_gp3_migration_max_size: - format: int64 - type: integer gcp_credentials: type: string irsa_role_arn: diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 4ea5b6319..ca835308c 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -350,16 +350,12 @@ configAwsOrGcp: # AWS region used to store EBS volumes aws_region: eu-central-1 - # enable automatic migration on AWS from gp2 to gp3 volumes - enable_ebs_gp3_migration: false - # defines maximum volume size in GB until which auto migration happens - # enable_ebs_gp3_migration_max_size: 1000 - # GCP credentials that will be used by the operator / pods # gcp_credentials: "" # AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods # kube_iam_role: "" + # Full ARN for IRSA (IAM Roles for Service Accounts) on EKS # irsa_role_arn: "" diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 02c6db8d0..caa1f6a1e 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -582,7 +582,7 @@ configuration they are grouped under the `kubernetes` key. 1. `ebs` : operator resizes EBS volumes directly and executes `resizefs` within a pod 2. `pvc` : operator only changes PVC definition 3. `off` : disables resize of the volumes. - 4. `mixed` : operator uses AWS API to adjust size, throughput, and IOPS, and calls pvc change for file system resize + 4. `mixed` : operator uses AWS API to adjust size, type, throughput, and IOPS, and calls pvc change for file system resize Default is "pvc". ## Kubernetes resource requests @@ -821,16 +821,6 @@ yet officially supported. Path to mount the above Secret in the filesystem of the container(s). The default is empty. -* **enable_ebs_gp3_migration** - enable automatic migration on AWS from gp2 to gp3 volumes, that are smaller - than the configured max size (see below). This ignores that EBS gp3 is by - default only 125 MB/sec vs 250 MB/sec for gp2 >= 333GB. - The default is `false`. - -* **enable_ebs_gp3_migration_max_size** - defines the maximum volume size in GB until which auto migration happens. - Default is 1000 (1TB) which matches 3000 IOPS. - ## Logical backup These parameters configure a K8s cron job managed by the operator to produce diff --git a/docs/user.md b/docs/user.md index 293ddf8c2..c1c2b8dc1 100644 --- a/docs/user.md +++ b/docs/user.md @@ -1129,7 +1129,7 @@ When using AWS with gp3 volumes you should set the mode to `mixed` because it will also adjust the IOPS and throughput that can be defined in the manifest. Check the [AWS docs](https://aws.amazon.com/ebs/general-purpose/) to learn about default and maximum values. Keep in mind that AWS rate-limits updating -volume specs to no more than once every 6 hours. +volume specs to no more than 4 times within 24 hours. ```yaml spec: diff --git a/manifests/complete-postgres-manifest.yaml b/manifests/complete-postgres-manifest.yaml index 93797e0e1..535f3a9f8 100644 --- a/manifests/complete-postgres-manifest.yaml +++ b/manifests/complete-postgres-manifest.yaml @@ -60,8 +60,8 @@ spec: volume: size: 1Gi # storageClass: my-sc -# iops: 1000 # for EBS gp3 -# throughput: 250 # in MB/s for EBS gp3 +# iops: 1000 +# throughput: 250 # in MB/s # selector: # matchExpressions: # - { key: flavour, operator: In, values: [ "banana", "chocolate" ] } diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index c7f4d2655..e85da7c48 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -41,8 +41,6 @@ data: enable_cross_namespace_secret: "false" enable_finalizers: "false" 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_maintenance_windows: "true" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index ebe4e7089..5fdf8aad8 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -66,11 +66,6 @@ spec: aws_region: default: eu-central-1 type: string - enable_ebs_gp3_migration: - type: boolean - enable_ebs_gp3_migration_max_size: - format: int64 - type: integer gcp_credentials: type: string irsa_role_arn: diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index e25a7b339..91f2b0888 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -167,8 +167,6 @@ configuration: # additional_secret_mount: "some-secret-name" # additional_secret_mount_path: "/some/dir" aws_region: eu-central-1 - enable_ebs_gp3_migration: false - # enable_ebs_gp3_migration_max_size: 1000 # gcp_credentials: "" # kube_iam_role: "" # irsa_role_arn: "" 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 046dd0761..02765b8f1 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -246,17 +246,15 @@ type LoadBalancerConfiguration struct { type AWSGCPConfiguration struct { WALES3Bucket string `json:"wal_s3_bucket,omitempty"` // +kubebuilder:default=eu-central-1 - AWSRegion string `json:"aws_region,omitempty"` - WALGSBucket string `json:"wal_gs_bucket,omitempty"` - GCPCredentials string `json:"gcp_credentials,omitempty"` - WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"` - LogS3Bucket string `json:"log_s3_bucket,omitempty"` - KubeIAMRole string `json:"kube_iam_role,omitempty"` - IRSARoleARN string `json:"irsa_role_arn,omitempty"` - AdditionalSecretMount string `json:"additional_secret_mount,omitempty"` - AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"` - EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration,omitempty"` - EnableEBSGp3MigrationMaxSize int64 `json:"enable_ebs_gp3_migration_max_size,omitempty"` + AWSRegion string `json:"aws_region,omitempty"` + WALGSBucket string `json:"wal_gs_bucket,omitempty"` + GCPCredentials string `json:"gcp_credentials,omitempty"` + WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"` + LogS3Bucket string `json:"log_s3_bucket,omitempty"` + KubeIAMRole string `json:"kube_iam_role,omitempty"` + IRSARoleARN string `json:"irsa_role_arn,omitempty"` + AdditionalSecretMount string `json:"additional_secret_mount,omitempty"` + AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"` } // OperatorDebugConfiguration defines options for the debug mode diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index ebe4e7089..5fdf8aad8 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -66,11 +66,6 @@ spec: aws_region: default: eu-central-1 type: string - enable_ebs_gp3_migration: - type: boolean - enable_ebs_gp3_migration_max_size: - format: int64 - type: integer gcp_credentials: type: string irsa_role_arn: diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 836bd086e..cb5b2a839 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -176,7 +176,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres cluster.eventRecorder = eventRecorder cluster.EBSVolumes = make(map[string]volumes.VolumeProperties) - if cfg.OpConfig.StorageResizeMode != "pvc" || cfg.OpConfig.EnableEBSGp3Migration { + if cfg.OpConfig.StorageResizeMode != "pvc" { cluster.VolumeResizer = &volumes.EBSVolumeResizer{AWSRegion: cfg.OpConfig.AWSRegion} } diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 5ada7ea9a..6e9f9348a 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -95,18 +95,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { 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 } - if c.OpConfig.EnableEBSGp3Migration && len(c.EBSVolumes) > 0 { - err = c.executeEBSMigration() - if nil != err { - return err - } - } - if err = c.syncPodServiceAccount(); err != nil { c.logger.Errorf("could not sync pod service account: %v", err) } @@ -1811,7 +1803,6 @@ func (c *Cluster) syncLogicalBackupJob() error { // no existing logical backup job, create new one c.logger.Info("could not find the cluster's logical backup job") - if err = c.createLogicalBackupJob(); err == nil { c.logger.Infof("created missing logical backup job %s", jobName) } else { diff --git a/pkg/cluster/volumes.go b/pkg/cluster/volumes.go index e32e558e6..c1762a073 100644 --- a/pkg/cluster/volumes.go +++ b/pkg/cluster/volumes.go @@ -11,7 +11,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/filesystems" @@ -79,9 +78,6 @@ func (c *Cluster) syncUnderlyingEBSVolume() error { } targetSize := quantityToGigabyte(newSize) - awsGp3 := aws.String("gp3") - awsIo2 := aws.String("io2") - errors := make([]string, 0) for _, volume := range c.EBSVolumes { @@ -106,20 +102,11 @@ func (c *Cluster) syncUnderlyingEBSVolume() error { modifySize = &targetSize } - if modifyIops != nil || modifyThroughput != nil || modifySize != nil { - if modifyIops != nil || modifyThroughput != nil { - // we default to gp3 if iops and throughput are configured - modifyType = awsGp3 - if targetValue.VolumeType == "io2" { - modifyType = awsIo2 - } - } else if targetValue.VolumeType == "gp3" && volume.VolumeType != "gp3" { - modifyType = awsGp3 - } else { - // do not touch type - modifyType = nil - } + if targetValue.VolumeType != "" && targetValue.VolumeType != volume.VolumeType { + modifyType = &targetValue.VolumeType + } + if modifyIops != nil || modifyThroughput != nil || modifySize != nil || modifyType != nil { err = c.VolumeResizer.ModifyVolume(volume.VolumeID, modifyType, modifySize, modifyIops, modifyThroughput) if err != nil { errors = append(errors, fmt.Sprintf("modify failed: %v, showing current EBS values: volume-id=%s size=%d iops=%d throughput=%d", err, volume.VolumeID, volume.Size, volume.Iops, volume.Throughput)) @@ -453,47 +440,3 @@ func getPodNameFromPersistentVolume(pv *v1.PersistentVolume) *spec.NamespacedNam func quantityToGigabyte(q resource.Quantity) int64 { return q.ScaledValue(0) / (1 * constants.Gigabyte) } - -func (c *Cluster) executeEBSMigration() error { - pvs, err := c.listPersistentVolumes() - if err != nil { - return fmt.Errorf("could not list persistent volumes: %v", err) - } - if len(pvs) == 0 { - c.logger.Warningf("no persistent volumes found - skipping EBS migration") - return nil - } - c.logger.Debugf("found %d volumes, size of known volumes %d", len(pvs), len(c.EBSVolumes)) - - if len(pvs) == len(c.EBSVolumes) { - hasGp2 := false - for _, v := range c.EBSVolumes { - if v.VolumeType == "gp2" { - hasGp2 = true - } - } - - if !hasGp2 { - c.logger.Debugf("no EBS gp2 volumes left to migrate") - return nil - } - } - - var i3000 int64 = 3000 - var i125 int64 = 125 - - for _, volume := range c.EBSVolumes { - if volume.VolumeType == "gp2" && volume.Size < c.OpConfig.EnableEBSGp3MigrationMaxSize { - c.logger.Infof("modifying EBS volume %s to type gp3 migration (%d)", volume.VolumeID, volume.Size) - err = c.VolumeResizer.ModifyVolume(volume.VolumeID, aws.String("gp3"), &volume.Size, &i3000, &i125) - if nil != err { - c.logger.Warningf("modifying volume %s failed: %v", volume.VolumeID, err) - } - } else { - c.logger.Debugf("skipping EBS volume %s to type gp3 migration (%d)", volume.VolumeID, volume.Size) - } - c.EBSVolumes[volume.VolumeID] = volume - } - - return nil -} diff --git a/pkg/cluster/volumes_test.go b/pkg/cluster/volumes_test.go index d6472539b..1f648712b 100644 --- a/pkg/cluster/volumes_test.go +++ b/pkg/cluster/volumes_test.go @@ -179,64 +179,6 @@ func CreatePVCs(namespace string, clusterName string, labels labels.Set, n int, return pvcList } -func TestMigrateEBS(t *testing.T) { - client, _ := newFakeK8sPVCclient() - clusterName := "acid-test-cluster" - namespace := "default" - - // new cluster with pvc storage resize mode and configured labels - var cluster = New( - Config{ - OpConfig: config.Config{ - Resources: config.Resources{ - ClusterLabels: map[string]string{"application": "spilo"}, - ClusterNameLabel: "cluster-name", - }, - StorageResizeMode: "pvc", - EnableEBSGp3Migration: true, - EnableEBSGp3MigrationMaxSize: 1000, - }, - }, client, acidv1.Postgresql{}, logger, eventRecorder) - cluster.Spec.Volume.Size = "1Gi" - - // set metadata, so that labels will get correct values - cluster.Name = clusterName - cluster.Namespace = namespace - filterLabels := cluster.labelsSet(false) - - testVolumes := []testVolume{testVol, testVol} - - initTestVolumesAndPods(cluster.KubeClient, namespace, clusterName, filterLabels, testVolumes) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - resizer := mocks.NewMockVolumeResizer(ctrl) - - resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-1")).Return("ebs-volume-1", nil) - resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-2")).Return("ebs-volume-2", nil) - - resizer.EXPECT().GetProviderVolumeID(gomock.Any()). - DoAndReturn(func(pv *v1.PersistentVolume) (string, error) { - return resizer.ExtractVolumeID(pv.Spec.AWSElasticBlockStore.VolumeID) - }). - Times(2) - - resizer.EXPECT().DescribeVolumes(gomock.Eq([]string{"ebs-volume-1", "ebs-volume-2"})).Return( - []volumes.VolumeProperties{ - {VolumeID: "ebs-volume-1", VolumeType: "gp2", Size: 100}, - {VolumeID: "ebs-volume-2", VolumeType: "gp3", Size: 100}}, nil) - - // expect only gp2 volume to be modified - resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - - cluster.VolumeResizer = resizer - err := cluster.populateVolumeMetaData() - assert.NoError(t, err) - err = cluster.executeEBSMigration() - assert.NoError(t, err) -} - func initTestVolumesAndPods(client k8sutil.KubernetesClient, namespace, clustername string, labels labels.Set, volumes []testVolume) { i := 0 for _, v := range volumes { @@ -287,7 +229,7 @@ func initTestVolumesAndPods(client k8sutil.KubernetesClient, namespace, clustern } } -func TestMigrateGp3Support(t *testing.T) { +func TestGp2Gp3Migration(t *testing.T) { client, _ := newFakeK8sPVCclient() clusterName := "acid-test-cluster" namespace := "default" @@ -300,73 +242,9 @@ func TestMigrateGp3Support(t *testing.T) { ClusterLabels: map[string]string{"application": "spilo"}, ClusterNameLabel: "cluster-name", }, - StorageResizeMode: "mixed", - EnableEBSGp3Migration: false, - EnableEBSGp3MigrationMaxSize: 1000, + StorageResizeMode: "mixed", }, - }, client, acidv1.Postgresql{}, logger, eventRecorder) - - cluster.Spec.Volume.Size = "150Gi" - cluster.Spec.Volume.Iops = aws.Int64(6000) - cluster.Spec.Volume.Throughput = aws.Int64(275) - - // set metadata, so that labels will get correct values - cluster.Name = clusterName - cluster.Namespace = namespace - filterLabels := cluster.labelsSet(false) - - testVolumes := []testVolume{testVol, testVol, testVol} - - initTestVolumesAndPods(cluster.KubeClient, namespace, clusterName, filterLabels, testVolumes) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - resizer := mocks.NewMockVolumeResizer(ctrl) - - resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-1")).Return("ebs-volume-1", nil) - resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-2")).Return("ebs-volume-2", nil) - resizer.EXPECT().ExtractVolumeID(gomock.Eq("aws://eu-central-1b/ebs-volume-3")).Return("ebs-volume-3", nil) - - resizer.EXPECT().GetProviderVolumeID(gomock.Any()). - DoAndReturn(func(pv *v1.PersistentVolume) (string, error) { - return resizer.ExtractVolumeID(pv.Spec.AWSElasticBlockStore.VolumeID) - }). - Times(3) - - resizer.EXPECT().DescribeVolumes(gomock.Eq([]string{"ebs-volume-1", "ebs-volume-2", "ebs-volume-3"})).Return( - []volumes.VolumeProperties{ - {VolumeID: "ebs-volume-1", VolumeType: "gp3", Size: 100, Iops: 3000}, - {VolumeID: "ebs-volume-2", VolumeType: "gp3", Size: 105, Iops: 4000}, - {VolumeID: "ebs-volume-3", VolumeType: "gp3", Size: 151, Iops: 6000, Throughput: 275}}, nil) - - // expect only gp2 volume to be modified - resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Eq(aws.Int64(150)), gomock.Eq(aws.Int64(6000)), gomock.Eq(aws.Int64(275))).Return(nil) - resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Eq(aws.String("gp3")), gomock.Eq(aws.Int64(150)), gomock.Eq(aws.Int64(6000)), gomock.Eq(aws.Int64(275))).Return(nil) - // resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-3"), gomock.Eq(aws.String("gp3")), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - - cluster.VolumeResizer = resizer - cluster.syncVolumes() -} - -func TestManualGp2Gp3Support(t *testing.T) { - client, _ := newFakeK8sPVCclient() - clusterName := "acid-test-cluster" - namespace := "default" - - // new cluster with pvc storage resize mode and configured labels - var cluster = New( - Config{ - OpConfig: config.Config{ - Resources: config.Resources{ - ClusterLabels: map[string]string{"application": "spilo"}, - ClusterNameLabel: "cluster-name", - }, - StorageResizeMode: "mixed", - EnableEBSGp3Migration: false, - EnableEBSGp3MigrationMaxSize: 1000, - }, - }, client, acidv1.Postgresql{}, logger, eventRecorder) + }, client, acidv1.Postgresql{Spec: acidv1.PostgresSpec{Volume: acidv1.Volume{VolumeType: "gp3"}}}, logger, eventRecorder) cluster.Spec.Volume.Size = "150Gi" cluster.Spec.Volume.Iops = aws.Int64(6000) @@ -409,7 +287,7 @@ func TestManualGp2Gp3Support(t *testing.T) { cluster.syncVolumes() } -func TestDontTouchType(t *testing.T) { +func TestNoVolumeTypeChange(t *testing.T) { client, _ := newFakeK8sPVCclient() clusterName := "acid-test-cluster" namespace := "default" @@ -422,9 +300,7 @@ func TestDontTouchType(t *testing.T) { ClusterLabels: map[string]string{"application": "spilo"}, ClusterNameLabel: "cluster-name", }, - StorageResizeMode: "mixed", - EnableEBSGp3Migration: false, - EnableEBSGp3MigrationMaxSize: 1000, + StorageResizeMode: "mixed", }, }, client, acidv1.Postgresql{}, logger, eventRecorder) diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 667384e30..0c237e8b4 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -181,8 +181,6 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.WALAZStorageAccount = fromCRD.AWSGCP.WALAZStorageAccount result.AdditionalSecretMount = fromCRD.AWSGCP.AdditionalSecretMount 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 * * *") diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 95324a747..538d44034 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -200,8 +200,6 @@ type Config struct { WALAZStorageAccount string `name:"wal_az_storage_account"` AdditionalSecretMount string `name:"additional_secret_mount"` 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"` EnableDBAccess bool `name:"enable_database_access" default:"true"` EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` diff --git a/pkg/util/volumes/ebs.go b/pkg/util/volumes/ebs.go index 212ee8ac4..49a4d5efa 100644 --- a/pkg/util/volumes/ebs.go +++ b/pkg/util/volumes/ebs.go @@ -89,14 +89,14 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti } for _, v := range volumeOutput.Volumes { - switch v.VolumeType { - case "gp3": - p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType), Iops: int64(*v.Iops), Throughput: int64(*v.Throughput)}) - case "gp2": - p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType)}) - default: - return nil, fmt.Errorf("discovered unexpected volume type %s %s", *v.VolumeId, v.VolumeType) + vp := VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType)} + if v.Iops != nil { + vp.Iops = int64(*v.Iops) } + if v.Throughput != nil { + vp.Throughput = int64(*v.Throughput) + } + p = append(p, vp) } return p, nil From d268c589c29df174c7e5bc76976c52c9edc3a384 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Mon, 27 Jul 2026 16:32:22 +0200 Subject: [PATCH 57/70] change volume specs to int32 and define max iops and throughput values (#3139) * change int64 volume specs to int32 like in used AWS lib * upper iops and throughput limits * minor refactoring in ebs.go --- .../templates/deployment.yaml | 4 +-- .../postgres-operator/crds/postgresqls.yaml | 6 ++-- docs/reference/cluster_manifest.md | 4 +-- manifests/postgresql.crd.yaml | 6 ++-- pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 6 ++-- pkg/apis/acid.zalan.do/v1/postgresql_type.go | 8 +++-- .../acid.zalan.do/v1/zz_generated.deepcopy.go | 4 +-- pkg/cluster/volumes.go | 20 +++++------ pkg/cluster/volumes_test.go | 25 +++++++------ pkg/util/volumes/ebs.go | 35 ++++++------------- pkg/util/volumes/volumes.go | 10 +++--- ui/manifests/deployment.yaml | 4 +-- ui/operator_ui/main.py | 4 +-- ui/run_local.sh | 4 +-- 14 files changed, 67 insertions(+), 73 deletions(-) diff --git a/charts/postgres-operator-ui/templates/deployment.yaml b/charts/postgres-operator-ui/templates/deployment.yaml index c8797e42e..d4a2a88b8 100644 --- a/charts/postgres-operator-ui/templates/deployment.yaml +++ b/charts/postgres-operator-ui/templates/deployment.yaml @@ -81,8 +81,8 @@ spec: "cost_memory": 0.014375, "free_iops": 3000, "free_throughput": 125, - "limit_iops": 16000, - "limit_throughput": 1000, + "limit_iops": 80000, + "limit_throughput": 2000, "postgresql_versions": [ "18", "17", diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 0e3558638..f3dcbab59 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -3258,7 +3258,8 @@ spec: description: Volume describes a single volume in the manifest. properties: iops: - format: int64 + format: int32 + maximum: 80000 type: integer isSubPathExpr: type: boolean @@ -3319,7 +3320,8 @@ spec: subPath: type: string throughput: - format: int64 + format: int32 + maximum: 2000 type: integer type: type: string diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index 0717e411f..7dddd5951 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -554,11 +554,11 @@ properties of the persistent storage that stores Postgres data. * **iops** When running the operator on AWS the latest generation of EBS volumes (`gp3`) - allows for configuring the number of IOPS. Maximum is 16000. Optional. + allows for configuring the number of IOPS. Maximum is 80000. Optional. * **throughput** When running the operator on AWS the latest generation of EBS volumes (`gp3`) - allows for configuring the throughput in MB/s. Maximum is 1000. Optional. + allows for configuring the throughput in MB/s. Maximum is 2000. Optional. * **selector** A label query over PVs to consider for binding. See the [Kubernetes diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 0e3558638..f3dcbab59 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -3258,7 +3258,8 @@ spec: description: Volume describes a single volume in the manifest. properties: iops: - format: int64 + format: int32 + maximum: 80000 type: integer isSubPathExpr: type: boolean @@ -3319,7 +3320,8 @@ spec: subPath: type: string throughput: - format: int64 + format: int32 + maximum: 2000 type: integer type: type: string diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index 0e3558638..f3dcbab59 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -3258,7 +3258,8 @@ spec: description: Volume describes a single volume in the manifest. properties: iops: - format: int64 + format: int32 + maximum: 80000 type: integer isSubPathExpr: type: boolean @@ -3319,7 +3320,8 @@ spec: subPath: type: string throughput: - format: int64 + format: int32 + maximum: 2000 type: integer type: type: string diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index a03bb13cc..2ea8c6ba8 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -167,9 +167,11 @@ type Volume struct { StorageClass string `json:"storageClass,omitempty"` SubPath string `json:"subPath,omitempty"` IsSubPathExpr *bool `json:"isSubPathExpr,omitempty"` - Iops *int64 `json:"iops,omitempty"` - Throughput *int64 `json:"throughput,omitempty"` - VolumeType string `json:"type,omitempty"` + // +kubebuilder:validation:Maximum=80000 + Iops *int32 `json:"iops,omitempty"` + // +kubebuilder:validation:Maximum=2000 + Throughput *int32 `json:"throughput,omitempty"` + VolumeType string `json:"type,omitempty"` } // AdditionalVolume specs additional optional volumes for statefulset 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 60003adfa..2a3fefd00 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -1686,12 +1686,12 @@ func (in *Volume) DeepCopyInto(out *Volume) { } if in.Iops != nil { in, out := &in.Iops, &out.Iops - *out = new(int64) + *out = new(int32) **out = **in } if in.Throughput != nil { in, out := &in.Throughput, &out.Throughput - *out = new(int64) + *out = new(int32) **out = **in } return diff --git a/pkg/cluster/volumes.go b/pkg/cluster/volumes.go index c1762a073..e4451a12c 100644 --- a/pkg/cluster/volumes.go +++ b/pkg/cluster/volumes.go @@ -81,24 +81,24 @@ func (c *Cluster) syncUnderlyingEBSVolume() error { errors := make([]string, 0) for _, volume := range c.EBSVolumes { - var modifyIops *int64 - var modifyThroughput *int64 - var modifySize *int64 + var modifyIops *int32 + var modifyThroughput *int32 + var modifySize *int32 var modifyType *string - if targetValue.Iops != nil && *targetValue.Iops >= int64(3000) { - if volume.Iops != int64(*targetValue.Iops) { + if targetValue.Iops != nil && *targetValue.Iops >= int32(3000) { + if volume.Iops != int32(*targetValue.Iops) { modifyIops = targetValue.Iops } } - if targetValue.Throughput != nil && *targetValue.Throughput >= int64(125) { - if volume.Throughput != int64(*targetValue.Throughput) { + if targetValue.Throughput != nil && *targetValue.Throughput >= int32(125) { + if volume.Throughput != int32(*targetValue.Throughput) { modifyThroughput = targetValue.Throughput } } - if targetSize > int64(volume.Size) { + if targetSize > volume.Size { modifySize = &targetSize } @@ -437,6 +437,6 @@ func getPodNameFromPersistentVolume(pv *v1.PersistentVolume) *spec.NamespacedNam return &spec.NamespacedName{Namespace: namespace, Name: name} } -func quantityToGigabyte(q resource.Quantity) int64 { - return q.ScaledValue(0) / (1 * constants.Gigabyte) +func quantityToGigabyte(q resource.Quantity) int32 { + return int32(q.ScaledValue(0) / (1 * constants.Gigabyte)) } diff --git a/pkg/cluster/volumes_test.go b/pkg/cluster/volumes_test.go index 1f648712b..51ac92e06 100644 --- a/pkg/cluster/volumes_test.go +++ b/pkg/cluster/volumes_test.go @@ -13,7 +13,6 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "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" @@ -25,9 +24,9 @@ import ( ) type testVolume struct { - size int64 - iops int64 - throughtput int64 + size int32 + iops int32 + throughtput int32 volType string } @@ -121,7 +120,7 @@ func TestQuantityToGigabyte(t *testing.T) { tests := []struct { name string quantityStr string - expected int64 + expected int32 }{ { "test with 1Gi", @@ -131,12 +130,12 @@ func TestQuantityToGigabyte(t *testing.T) { { "test with float", "1.5Gi", - int64(1), + int32(1), }, { "test with 1000Mi", "1000Mi", - int64(0), + int32(0), }, } @@ -247,8 +246,8 @@ func TestGp2Gp3Migration(t *testing.T) { }, client, acidv1.Postgresql{Spec: acidv1.PostgresSpec{Volume: acidv1.Volume{VolumeType: "gp3"}}}, logger, eventRecorder) cluster.Spec.Volume.Size = "150Gi" - cluster.Spec.Volume.Iops = aws.Int64(6000) - cluster.Spec.Volume.Throughput = aws.Int64(275) + cluster.Spec.Volume.Iops = aws.Int32(6000) + cluster.Spec.Volume.Throughput = aws.Int32(275) // set metadata, so that labels will get correct values cluster.Name = clusterName @@ -280,8 +279,8 @@ func TestGp2Gp3Migration(t *testing.T) { }, nil) // expect only gp2 volume to be modified - resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int64(6000)), gomock.Eq(aws.Int64(275))).Return(nil) - resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int64(6000)), gomock.Eq(aws.Int64(275))).Return(nil) + resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int32(6000)), gomock.Eq(aws.Int32(275))).Return(nil) + resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Eq(aws.String("gp3")), gomock.Nil(), gomock.Eq(aws.Int32(6000)), gomock.Eq(aws.Int32(275))).Return(nil) cluster.VolumeResizer = resizer cluster.syncVolumes() @@ -343,8 +342,8 @@ func TestNoVolumeTypeChange(t *testing.T) { }, nil) // expect only gp2 volume to be modified - resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Nil(), gomock.Eq(aws.Int64(177)), gomock.Nil(), gomock.Nil()).Return(nil) - resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Nil(), gomock.Eq(aws.Int64(177)), gomock.Nil(), gomock.Nil()).Return(nil) + resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-1"), gomock.Nil(), gomock.Eq(aws.Int32(177)), gomock.Nil(), gomock.Nil()).Return(nil) + resizer.EXPECT().ModifyVolume(gomock.Eq("ebs-volume-2"), gomock.Nil(), gomock.Eq(aws.Int32(177)), gomock.Nil(), gomock.Nil()).Return(nil) cluster.VolumeResizer = resizer cluster.syncVolumes() diff --git a/pkg/util/volumes/ebs.go b/pkg/util/volumes/ebs.go index 49a4d5efa..7962a50b3 100644 --- a/pkg/util/volumes/ebs.go +++ b/pkg/util/volumes/ebs.go @@ -89,12 +89,12 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti } for _, v := range volumeOutput.Volumes { - vp := VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType)} + vp := VolumeProperties{VolumeID: *v.VolumeId, Size: int32(*v.Size), VolumeType: string(v.VolumeType)} if v.Iops != nil { - vp.Iops = int64(*v.Iops) + vp.Iops = int32(*v.Iops) } if v.Throughput != nil { - vp.Throughput = int64(*v.Throughput) + vp.Throughput = int32(*v.Throughput) } p = append(p, vp) } @@ -103,7 +103,7 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti } // ResizeVolume actually calls AWS API to resize the EBS volume if necessary. -func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { +func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int32) error { /* first check if the volume is already of a requested size */ volumeOutput, err := r.connection.DescribeVolumes(context.TODO(), &ec2.DescribeVolumesInput{VolumeIds: []string{volumeID}}) if err != nil { @@ -113,12 +113,11 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { if *vol.VolumeId != volumeID { return fmt.Errorf("describe volume %q returned information about a non-matching volume %q", volumeID, *vol.VolumeId) } - sizeInt32 := int32(newSize) - if *vol.Size == sizeInt32 { + if *vol.Size == newSize { // nothing to do return nil } - input := ec2.ModifyVolumeInput{Size: &sizeInt32, VolumeId: &volumeID} + input := ec2.ModifyVolumeInput{Size: &newSize, VolumeId: &volumeID} output, err := r.connection.ModifyVolume(context.TODO(), &input) if err != nil { return fmt.Errorf("could not modify persistent volume: %v", err) @@ -154,28 +153,16 @@ func (r *EBSVolumeResizer) ResizeVolume(volumeID string, newSize int64) error { } // ModifyVolume Modify EBS volume -func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error { - var sizeInt32 *int32 - var iopsInt32 *int32 - var throughputInt32 *int32 - - input := ec2.ModifyVolumeInput{ - VolumeId: &volumeID, - } +func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSize *int32, iops *int32, throughput *int32) error { + input := ec2.ModifyVolumeInput{VolumeId: &volumeID} if newSize != nil { - s := int32(*newSize) - sizeInt32 = &s - input.Size = sizeInt32 + input.Size = newSize } if iops != nil { - i := int32(*iops) - iopsInt32 = &i - input.Iops = iopsInt32 + input.Iops = iops } if throughput != nil { - t := int32(*throughput) - throughputInt32 = &t - input.Throughput = throughputInt32 + input.Throughput = throughput } if newType != nil { input.VolumeType = types.VolumeType(*newType) diff --git a/pkg/util/volumes/volumes.go b/pkg/util/volumes/volumes.go index 32f68c65e..501215b41 100644 --- a/pkg/util/volumes/volumes.go +++ b/pkg/util/volumes/volumes.go @@ -8,9 +8,9 @@ import v1 "k8s.io/api/core/v1" type VolumeProperties struct { VolumeID string VolumeType string - Size int64 - Iops int64 - Throughput int64 + Size int32 + Iops int32 + Throughput int32 } // VolumeResizer defines the set of methods used to implememnt provider-specific resizing of persistent volumes. @@ -20,8 +20,8 @@ type VolumeResizer interface { VolumeBelongsToProvider(pv *v1.PersistentVolume) bool GetProviderVolumeID(pv *v1.PersistentVolume) (string, error) ExtractVolumeID(volumeID string) (string, error) - ResizeVolume(providerVolumeID string, newSize int64) error - ModifyVolume(providerVolumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error + ResizeVolume(providerVolumeID string, newSize int32) error + ModifyVolume(providerVolumeID string, newType *string, newSize *int32, iops *int32, throughput *int32) error DisconnectFromProvider() error DescribeVolumes(providerVolumesID []string) ([]VolumeProperties, error) } diff --git a/ui/manifests/deployment.yaml b/ui/manifests/deployment.yaml index e19a850b8..eb48d9605 100644 --- a/ui/manifests/deployment.yaml +++ b/ui/manifests/deployment.yaml @@ -70,8 +70,8 @@ spec: "cost_memory": 0.014375, "free_iops": 3000, "free_throughput": 125, - "limit_iops": 16000, - "limit_throughput": 1000, + "limit_iops": 80000, + "limit_throughput": 2000, "postgresql_versions": [ "18", "17", diff --git a/ui/operator_ui/main.py b/ui/operator_ui/main.py index c91450d4c..469fe66ed 100644 --- a/ui/operator_ui/main.py +++ b/ui/operator_ui/main.py @@ -86,9 +86,9 @@ # maximum and limitation of IOPS and throughput FREE_IOPS = float(getenv('FREE_IOPS', 3000)) -LIMIT_IOPS = float(getenv('LIMIT_IOPS', 16000)) +LIMIT_IOPS = float(getenv('LIMIT_IOPS', 80000)) FREE_THROUGHPUT = float(getenv('FREE_THROUGHPUT', 125)) -LIMIT_THROUGHPUT = float(getenv('LIMIT_THROUGHPUT', 1000)) +LIMIT_THROUGHPUT = float(getenv('LIMIT_THROUGHPUT', 2000)) # get the default value of core and memory DEFAULT_MEMORY = getenv('DEFAULT_MEMORY', '300Mi') DEFAULT_MEMORY_LIMIT = getenv('DEFAULT_MEMORY_LIMIT', '300Mi') diff --git a/ui/run_local.sh b/ui/run_local.sh index 59729a92a..13d8f053a 100755 --- a/ui/run_local.sh +++ b/ui/run_local.sh @@ -28,8 +28,8 @@ default_operator_ui_config='{ "cost_memory": 0.014375, "free_iops": 3000, "free_throughput": 125, - "limit_iops": 16000, - "limit_throughput": 1000, + "limit_iops": 80000, + "limit_throughput": 2000, "postgresql_versions": [ "18", "17", From 4c1bb1c0ea6378a745dd6ca7d0396d3e51e61525 Mon Sep 17 00:00:00 2001 From: Juhani Pelli Date: Mon, 27 Jul 2026 18:36:42 +0300 Subject: [PATCH 58/70] Use maxUnavailable for the critical-op PDB to stop idle alert noise (#3141) * Use maxUnavailable for the critical-op PDB to stop idle alert noise The critical-op PDB is created with minAvailable equal to numberOfInstances while its selector (critical-operation=true) matches no pods during normal operation. This leaves status.desiredHealthy at N and currentHealthy at 0 permanently, so monitoring stacks fire alerts like kube-prometheus-stack's KubePdbNotEnoughHealthyPods for every idle cluster (#3020). maxUnavailable: 0 provides the same protection while a critical operation is running - no voluntary evictions of labeled pods - but keeps the budget satisfied (desiredHealthy 0) when nothing matches. When PDBs are disabled or there are no instances, the budget relaxes to maxUnavailable 100% instead of minAvailable 0. Fixes #3020 Co-Authored-By: Claude Fable 5 * Update PDB docs for critical-op maxUnavailable semantics Co-Authored-By: Claude Fable 5 * Clarify why the two PDBs use different budget fields Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 Co-authored-by: Felix Kunde --- docs/administrator.md | 17 +++++++++++++---- pkg/cluster/k8sres.go | 14 ++++++++++---- pkg/cluster/k8sres_test.go | 19 +++++++++++++++---- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/docs/administrator.md b/docs/administrator.md index adfee6065..f7f0cb982 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -639,9 +639,11 @@ masters in single-node clusters and/or the last remaining running instance in a cluster. ## PDB for critical operations -The `MinAvailable` parameter of this PDB is equal to the `numberOfInstances` set in the -cluster manifest, while label selector includes `critical-operation=true` condition. This -allows to protect all pods of a cluster, given they are labeled accordingly. +The `MaxUnavailable` parameter of this PDB is set to `0`, while label selector includes +`critical-operation=true` condition. This blocks voluntary disruptions for all pods of a +cluster that are labeled accordingly, without leaving an unsatisfiable budget behind when +no pods carry the label (which previously kept monitoring alerts like +`KubePdbNotEnoughHealthyPods` firing permanently). For example, Operator labels all Spilo pods with `critical-operation=true` during the major version upgrade run. You may want to protect cluster pods during other critical operations by assigning the label to pods yourself or using other means of automation. @@ -651,7 +653,14 @@ The PDB is only relaxed in two scenarios: * If a cluster is scaled down to `0` instances (e.g. for draining nodes) * If the PDB is disabled in the configuration (`enable_pod_disruption_budget`) -The PDBs are still in place having `MinAvailable` set to `0`. Disabling PDBs +The PDBs are still in place but fully relaxed: the primary PDB with `MinAvailable` +set to `0` and the critical operations PDB with `MaxUnavailable` set to `100%`. +The two PDBs intentionally use different budget fields matching their purposes: +the primary PDB guarantees a minimum count of always-present pods +(`MinAvailable`), while the critical operations PDB freezes disruptions for +whatever pods currently carry the `critical-operation=true` label - a +usually-empty set, which `MaxUnavailable: 0` expresses without producing an +unsatisfiable budget while idle. Disabling PDBs helps avoiding blocking Kubernetes upgrades in managed K8s environments at the cost of prolonged DB downtime. See PR [#384](https://github.com/zalando/postgres-operator/pull/384) for the use case. diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 4814a5546..5af6ead45 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -2315,12 +2315,18 @@ func (c *Cluster) generatePrimaryPodDisruptionBudget() *policyv1.PodDisruptionBu } func (c *Cluster) generateCriticalOpPodDisruptionBudget() *policyv1.PodDisruptionBudget { - minAvailable := intstr.FromInt32(c.Spec.NumberOfInstances) + // MaxUnavailable: 0 blocks voluntary disruption of any pod carrying the + // critical-operation label, while keeping the budget satisfied when no + // pod matches (status.desiredHealthy stays 0 outside critical + // operations). The previous MinAvailable: N spec left desiredHealthy at + // N with zero matching pods during normal operation, permanently firing + // alerts like kube-prometheus-stack's KubePdbNotEnoughHealthyPods (#3020). + maxUnavailable := intstr.FromInt32(0) pdbEnabled := c.OpConfig.EnablePodDisruptionBudget - // if PodDisruptionBudget is disabled or if there are no DB pods, set the budget to 0. + // if PodDisruptionBudget is disabled or if there are no DB pods, allow all disruptions. if (pdbEnabled != nil && !(*pdbEnabled)) || c.Spec.NumberOfInstances <= 0 { - minAvailable = intstr.FromInt(0) + maxUnavailable = intstr.FromString("100%") } labels := c.labelsSet(false) @@ -2335,7 +2341,7 @@ func (c *Cluster) generateCriticalOpPodDisruptionBudget() *policyv1.PodDisruptio OwnerReferences: c.ownerReferences(), }, Spec: policyv1.PodDisruptionBudgetSpec{ - MinAvailable: &minAvailable, + MaxUnavailable: &maxUnavailable, Selector: &metav1.LabelSelector{ MatchLabels: labels, }, diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index c9ebc020f..d7afd80cc 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -2556,6 +2556,17 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { } } + hasMaxUnavailable := func(expected string) func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + return func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + actual := podDisruptionBudget.Spec.MaxUnavailable.String() + if actual != expected { + return fmt.Errorf("PodDisruptionBudget MaxUnavailable is incorrect, got %s, expected %s", + actual, expected) + } + 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 @@ -2749,7 +2760,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ testPodDisruptionBudgetOwnerReference, hasName("postgres-myapp-database-critical-op-pdb"), - hasMinAvailable(3), + hasMaxUnavailable("0"), testLabelsAndSelectors(false), }, }, @@ -2766,7 +2777,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ testPodDisruptionBudgetOwnerReference, hasName("postgres-myapp-database-critical-op-pdb"), - hasMinAvailable(0), + hasMaxUnavailable("100%"), testLabelsAndSelectors(false), }, }, @@ -2783,7 +2794,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ testPodDisruptionBudgetOwnerReference, hasName("postgres-myapp-database-critical-op-pdb"), - hasMinAvailable(0), + hasMaxUnavailable("100%"), testLabelsAndSelectors(false), }, }, @@ -2800,7 +2811,7 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ testPodDisruptionBudgetOwnerReference, hasName("postgres-myapp-database-critical-op-pdb"), - hasMinAvailable(3), + hasMaxUnavailable("0"), testLabelsAndSelectors(false), }, }, From 8eda2cd1a2a5d37f21b1bfdeb34a0620e0180235 Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Mon, 27 Jul 2026 23:50:17 +0200 Subject: [PATCH 59/70] bump to v2.0 (#3134) * bump to v2.0 * update docs and remove some diagrams * update crd in charts dir * update helm charts * reflect docs feedback --- .../postgres-operator-issue-template.md | 2 +- README.md | 11 +- charts/postgres-operator-ui/Chart.yaml | 4 +- charts/postgres-operator-ui/index.yaml | 58 +++++----- .../postgres-operator-ui-1.10.1.tgz | Bin 5016 -> 0 bytes .../postgres-operator-ui-2.0.0.tgz | Bin 0 -> 5084 bytes charts/postgres-operator-ui/values.yaml | 2 +- charts/postgres-operator/Chart.yaml | 4 +- .../crds/operatorconfigurations.yaml | 4 +- charts/postgres-operator/index.yaml | 58 +++++----- .../postgres-operator-1.10.1.tgz | Bin 17424 -> 0 bytes .../postgres-operator-2.0.0.tgz | Bin 0 -> 42996 bytes charts/postgres-operator/values.yaml | 6 +- docs/administrator.md | 19 ++-- docs/diagrams/operator.png | Bin 259075 -> 0 bytes docs/diagrams/operator.tex | 101 ------------------ docs/diagrams/pod.png | Bin 241573 -> 0 bytes docs/diagrams/pod.tex | 92 ---------------- docs/index.md | 11 +- docs/migrate.md | 7 +- docs/reference/operator_parameters.md | 2 +- e2e/run.sh | 2 +- e2e/tests/test_e2e.py | 2 +- manifests/complete-postgres-manifest.yaml | 2 +- manifests/configmap.yaml | 4 +- manifests/operatorconfiguration.crd.yaml | 4 +- manifests/postgres-operator.yaml | 2 +- ...gresql-operator-default-configuration.yaml | 4 +- mkdocs.yml | 1 + .../v1/operator_configuration_type.go | 4 +- .../v1/operatorconfiguration.crd.yaml | 4 +- pkg/cluster/cluster_test.go | 4 +- pkg/controller/operator_config.go | 4 +- pkg/util/config/config.go | 4 +- ui/app/package.json | 2 +- ui/manifests/deployment.yaml | 2 +- 36 files changed, 117 insertions(+), 309 deletions(-) delete mode 100644 charts/postgres-operator-ui/postgres-operator-ui-1.10.1.tgz create mode 100644 charts/postgres-operator-ui/postgres-operator-ui-2.0.0.tgz delete mode 100644 charts/postgres-operator/postgres-operator-1.10.1.tgz create mode 100644 charts/postgres-operator/postgres-operator-2.0.0.tgz delete mode 100644 docs/diagrams/operator.png delete mode 100644 docs/diagrams/operator.tex delete mode 100644 docs/diagrams/pod.png delete mode 100644 docs/diagrams/pod.tex diff --git a/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md b/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md index 3b731eb72..8e676f799 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.15.1 +- **Which image of the operator are you using?** e.g. ghcr.io/zalando/postgres-operator:v2.0.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/README.md b/README.md index 31e9c1748..a61af4568 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,13 @@ pipelines with no access to Kubernetes API directly, promoting infrastructure as * Pod protection during bootstrap phase and configurable maintenance windows * Restore and cloning Postgres clusters on AWS, GCS and Azure * Additionally logical backups to S3 or GCS bucket can be configured -* Standby cluster from S3 or GCS WAL archive +* Standby cluster from S3 or GCS WAL archive or remote host * Configurable for non-cloud environments * Basic credential and user management on K8s, eases application deployments * Support for custom TLS certificates * UI to create and edit Postgres cluster manifests * Compatible with OpenShift +* Multi-arch support ### PostgreSQL features @@ -63,23 +64,21 @@ production for over five years. | Release | Postgres versions | K8s versions | Golang | | :-------- | :---------------: | :---------------: | :-----: | -| next | 14 → 18 | 1.27+ | 1.26.4 | +| v2.0.0 | 14 → 18 | 1.27+ | 1.26.4 | | v1.15.1 | 13 → 17 | 1.27+ | 1.25.3 | | v1.14.0 | 13 → 17 | 1.27+ | 1.23.4 | | 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 | ## Getting started For a quick first impression follow the instructions of this [tutorial](docs/quickstart.md). -## Supported setups of Postgres and Applications +## Migrating from v1 to v2 operator -![Features](docs/diagrams/neutral_operator_dark.png#gh-dark-mode-only) -![Features](docs/diagrams/neutral_operator_light.png#gh-light-mode-only) +If you have been using Postgres Operator since v1.x (thank you), make sure you have read the [migration docs](docs/migrate.md) before deploying a v2 operator. ## Documentation diff --git a/charts/postgres-operator-ui/Chart.yaml b/charts/postgres-operator-ui/Chart.yaml index 871640467..7dba22103 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.15.1 -appVersion: 1.15.1 +version: 2.0.0 +appVersion: 2.0.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 20408aeaf..fb01475ee 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: 2.0.0 + created: "2026-07-27T16:59:53.405736+02:00" + description: Postgres Operator UI provides a graphical interface for a convenient + database-as-a-service user experience + digest: 37d2392421822a6dfeed20f3ea40420c52e4e4aaf90354d20aa27175fbad6c29 + 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-2.0.0.tgz + version: 2.0.0 - apiVersion: v2 appVersion: 1.15.1 - created: "2025-12-11T12:44:25.470723322+01:00" + created: "2026-07-27T16:59:53.405452+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: 4bbb750934366038d692711f924151182b7be131b6822d011f5a4e51cf609482 @@ -26,7 +49,7 @@ entries: version: 1.15.1 - apiVersion: v2 appVersion: 1.14.0 - created: "2025-12-11T12:44:25.468680645+01:00" + created: "2026-07-27T16:59:53.405196+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: e87ed898079a852957a67a4caf3fbd27b9098e413f5d961b7a771a6ae8b3e17c @@ -49,7 +72,7 @@ entries: version: 1.14.0 - apiVersion: v2 appVersion: 1.13.0 - created: "2025-12-11T12:44:25.466716836+01:00" + created: "2026-07-27T16:59:53.404946+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: e0444e516b50f82002d1a733527813c51759a627cefdd1005cea73659f824ea8 @@ -72,7 +95,7 @@ entries: version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2025-12-11T12:44:25.464739895+01:00" + created: "2026-07-27T16:59:53.404693+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: cbcef400c23ccece27d97369ad629278265c013e0a45c0b7f33e7568a082fedd @@ -95,7 +118,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2025-12-11T12:44:25.462698399+01:00" + created: "2026-07-27T16:59:53.40439+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: a45f2284045c2a9a79750a36997386444f39b01ac722b17c84b431457577a3a2 @@ -116,27 +139,4 @@ entries: urls: - postgres-operator-ui-1.11.0.tgz version: 1.11.0 - - apiVersion: v2 - appVersion: 1.10.1 - created: "2025-12-11T12:44:25.460357063+01:00" - description: Postgres Operator UI provides a graphical interface for a convenient - database-as-a-service user experience - digest: 2e5e7a82aebee519ec57c6243eb8735124aa4585a3a19c66ffd69638fbeb11ce - 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.10.1.tgz - version: 1.10.1 -generated: "2025-12-11T12:44:25.45732896+01:00" +generated: "2026-07-27T16:59:53.404012+02:00" diff --git a/charts/postgres-operator-ui/postgres-operator-ui-1.10.1.tgz b/charts/postgres-operator-ui/postgres-operator-ui-1.10.1.tgz deleted file mode 100644 index c0719f7bf222094a686a773ca38a9c19510dacc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5016 zcmV;J6KCuniwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PJ1=bK5qu@6Y@z_UOBLZLg&GO%$cM-aOY)JDQ}n#hXgm;15|$Qufd5$upogdcBt(e$-{mc( z{IzRYDP?8J@`F0H+?bK4od2Ece@Ws3*^>dlf%We?R&R6tcYEE_`hSjc1}`uoMa-d` zbh%o0DTS__!Ofg9pkkf>Zyi60{ao`R2%5ymhS zn2^GKTI9fSk;Q}~(-kGhET#duNQ;CIz*Lko0TvOR6*)pdfiB1l75VQ#0myMi8PD$r zt1ErIGIhC%^e~l#S;{C+1t|$=xV^H(q6FY%Hh^Wdwg9psj>lr)%Zm!CIbu|E<7pN!|#GeO&oaH@i2~1jahZze#H`UmQ@fYK`!Gov5u;`L>6hV@5V90?a zVZ};lv|#ofkdhB&FvCQs2!>EFn#?5MsEA|vyjI>C``a2T3}PCtM@Ly*iEhVqLAksF z2(n@TmSHU9uLUlos#=EeGnMN5TY;=aYi`n-l6JHC;pJ;*aR$R}=BOOKw z!<@@nwRvyOQIi4G6iPM0@^OW&{XLzQnz^dALMq89yD_hhs znl&!sf^p0*$pT*yA4T1uyrl`bR5{?)JiVrj`Y{eb{gK2>KI0^xA-}9w=m6BPgiZMn z$)cJj4O9U&5-Nsoc}}D*kR(Yt5x~KeEDg`sKSix~5A7L5S+AbQnveJ)XXi~$IVJ)$ z1S}+&^Afceq?}3;h8TiM#H^`8`^9)?Y4{%uFj=mlSvh+QUpQfRtV5GuG2ohT*EEtz zm=|`oi}NI7pElDRuj_6(okEP zowHpHijLflDm11+yOR)*M8HRs7D*@-?ZX_(jvuFfXF-IbbPC$3?@tTb5niI6-79jw zhShTW=fC8d7`_!4A=RBB=QNo~Q@9{^2rMBPo2ML>6yJd8tw5N{2ozMHVN$`)`7aa6 zt^#HF<^0ufIwk% zT@0!l0HyVLiTBqSt?$;Z^WW-WU&BIK0p3tP7wrH~?=bm$qvjpn?~|Y@-(+}kJ-lfU zeHIyaOVO0`#qh`T+gCT!>)}tMtIN+KAHE>k0(sBfnnlz#qhfUt?CxWeXH)a=l*}SJL_Ct-3%u>f6G6mjYIK2Yb*ZOGi>X$|9_5h zlfn{nv5n<(!nb$+5~#!waRFa>kqi_CBVnko9A$WTfEfzD zAp7|1X4-e|+9<%>T<-q!%&N0p~XFAV5J`{Lh{acP< zZm$2H>7LgAbCh@QI^V*QE(S6v644lifAsxA3`;P_0estecu)kDGJKnf{UzTBx5&7G zu2ggylzYQRSFyGxm7zzyJa7`I0s8AQ_$(-|mYC->M0hXUl_UVymX~xfDWZtJ1yx%! z!z7f~rLY%eXosxYh>AGAhu;bkm+lX=L<(O?d)zqJv8wg(&}cQZ^~2Th)--zwjvD`%tvUT!_GR$>@l)K8b2Tg_mzGso z%i6m8!2)JEO?U+AUzqk6rfzU6+x-xw+jqU8jP-tlRI$kqnbLL$^|CR!t3m#nCP7?; zhm}JAW;F?IwsKD3u}(BpRiHt2HYL=wx$P^dV&Y?bZ2x2xb?*c<^{2e94l3b2U2+66zx>D@bNbiOQ`b3Fe0Gzn=k`v@~|2>)$kBmQGqX1903 z|K})w5dQyAD$B_XJgFH-W30}7YD{`$UKD_Bv;66^zzY9Y47fB$CI5E5#gj~QvOZ|v z7gqc-;q>3G9Kipz(fovLzytW-GrH!c|L-`b_}{aXCd3QBX^rMDR#V?X@&D%-+(5Sj zA~vEe$AcQnFesB8WwH$$fT;j5j4|M8UXJ=gc+amI)9{27@i7BZw@^vf&=pbQmY`26 z>c>Hcy0cbX_7e`)tr|38=E%o z(+p+ZT*HUkOzT_|ZYO-!3|1Z5$KyR5>?XiE6?HvYtBjAT6YAm zPlGfbfO>N=ZiD15GHWBz}Q0 zxu0N=CLtR@w?)F~0@H%8o|-K&8@tnYxI&nT8Pj9bZ zZFZ_k{%K4TCQGK5>7i}j4Q5y4;r02=)%EmzJo*5x?UYh@55E;D$HT2$ytL-HxUgSC>DIemZ_!-t7=h4by;4W18Hl1GUa~-LfsZfFnLf zK`d-1CLQVZ^NzY}IMgs<(=yR`%6LwbnJU2Bc+lUO4|{4sxL}wcMHr-vPtj-U01Vye zcdh+YG|eh9!`)BC=XqMp=2^i@TBr92aggR%l68CC{bc1E2PL8D*n3E$9I+PCHVk7w zQAUZt5xSr{&s zm|vuejN}OF-@i9?v#%K{=ocg-K8-1-$n=+mk5%YO|B3QC8${bUw@8pzuh=xsD^}@1 zdzy2ev^gI+G21Cf%_{hoV2ej}n8u6U$squD(T^0?{{FDHJrd)iM~zkkt)6|L&X z`%RBQ+uT&`3lue9%n()9zqdxPRkzB6cuew^Q1!bbq78AiEt-6Pbk%|Kb&^)3uWCg; zX?sUn6`GNK-twCs1+qUM4J}QkqPZLSn$tMPh`wz-^ht1sNw^t3YP=!UF8jcvU1WG^ zhijS?3m@~cehuxrT*n&jj+gZE2)QcKGX8c*geuK%^WGzR_PI^+vz7z?Uu~rSNdM1t zO?S)xvrhisvy|5Nf9j*Z%C?@){T}rf>b3EN{=zezE;i0JwCV_<;bgQN^A&WK(cP^x zkIK~`NeEx#Z(BU%%eHF$ZmqhNcCN49m|Zore%W=0OxxsJ&c*ul<>q3%b$I$QCx%a? z?&T}WJ31u?No;^VgXuXA|Eu>EKGF;vz<;A>8XNf^rqw&;e>_WR=l^ZI46-{DaMMiG ziC<5f>v!749)V7uo&&dY{p(@`=qyQddqwEnx+rvKON+NbsZEaeQwgmcUj`F)yl z&dV>*_yvtan#>?0!5x_)TYUkJ6`AxPfXy+EVHT%;nUqA6*>{j*EWfKL_p6QjBng!> zNN^@SBltR#n^+7>r|M7N=x~+9_mC#CrVt4-%ps-;>WcniGM(@=N97DI(#0Z8;Pu4> zLYgy0pHbeC|CZt_y8nCLk^j~==Ch9Yr~b*7NoUPSY)FetzWd6QZ*}%2Q@+)Ga;JQ& z^F^k7`#;JVye2tK3kIVXL#F6ip8kpfS9BU8>6CPN`m3TZS&)X<`8V1^?p*)EKYjaL z-+wht$KL+_YwxuGdzP|c*fHTbEn{rEG^2jMruEI(%5b&68*9kh)mQrlpjw7yYlfp) z#*NtyF{}y?)p0|JASthEbk@{t=&>m3)u-bZ?_or&z==G=_Xsgvv|ZODRuuI7o`r_#_^wf=TYbbgY@z%rQ}rF&dXrXt z%eMU4qLtsYEiYQ%wS_uJDZg!7{wlw33o?t8wzKsPe&-fo^INy;J9|E@3D8O;`@rws ziYi4OZw*4T5v|lVozH%kRLdVHDZM zNm-vsSv}p_rL3h69;K}2QOas|yPjjaU#7B#mTNl}+Fln)*Ca;dM?MM!?e+V8&$Y4d zczxG(2zrK%q#JbIka%9d7y6zPTFyyXpG#TI-O6fiE33IvS&wrA49D_p?+evchxB~M z2rN4aO}FPcW)N7O?N}rV{jiHc;2C|>GyA4Zx`AyPt{Hf?Y4=U97nzQC($(kE)tz2| zx62FIrK`Q;fYs>RX0LC4p)PB`M=&&r8QQ%d>_(pBn24m`wXDeYuC;wyFtI-_1ykRS)WH)d%JjtmEQT`~7xrmot($R}ps^=zjX5)#^d zJM=>{a1mYX`Yy4Y&@}qpsNb^!!!#_j-|O0D;96loLUIDuXVcZ4z$#R5^`AQIRlsiF ibF5W3>T}2DPUTci5rqhxD diff --git a/charts/postgres-operator-ui/postgres-operator-ui-2.0.0.tgz b/charts/postgres-operator-ui/postgres-operator-ui-2.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..8faf3015576082ac8c1ae394c2a673dfca9a6526 GIT binary patch literal 5084 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 literal 0 HcmV?d00001 diff --git a/charts/postgres-operator-ui/values.yaml b/charts/postgres-operator-ui/values.yaml index c308335b2..4f15dd565 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.15.1 + tag: v2.0.0 pullPolicy: "IfNotPresent" # Optionally specify an array of imagePullSecrets. diff --git a/charts/postgres-operator/Chart.yaml b/charts/postgres-operator/Chart.yaml index 6f0d2e762..e33d783eb 100644 --- a/charts/postgres-operator/Chart.yaml +++ b/charts/postgres-operator/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: postgres-operator -version: 1.15.1 -appVersion: 1.15.1 +version: 2.0.0 +appVersion: 2.0.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 5fdf8aad8..68a934cf9 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -137,7 +137,7 @@ spec: type: boolean type: object docker_image: - default: ghcr.io/zalando/spilo-18:4.1-p1 + default: ghcr.io/zalando/spilo-18:4.1-p2 type: string enable_crd_registration: default: true @@ -644,7 +644,7 @@ spec: logical_backup_cronjob_environment_secret: type: string logical_backup_docker_image: - default: ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1 + default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0 type: string logical_backup_failed_jobs_history_limit: default: 3 diff --git a/charts/postgres-operator/index.yaml b/charts/postgres-operator/index.yaml index 7128b8eb9..7f54a08c4 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: 2.0.0 + created: "2026-07-27T16:59:31.277476+02:00" + description: Postgres Operator creates and manages PostgreSQL clusters running + in Kubernetes + digest: 488ae36a71c075ebee4b759f52db31029d559cab20efb329cf151f206087deaf + 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-2.0.0.tgz + version: 2.0.0 - apiVersion: v2 appVersion: 1.15.1 - created: "2025-12-17T14:48:33.832345061+01:00" + created: "2026-07-27T16:59:31.276117+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 9f3edc3d796105c02c04eaae28a78e58fb08c1847a9de012245fd6ac2c0d2c00 @@ -25,7 +47,7 @@ entries: version: 1.15.1 - apiVersion: v2 appVersion: 1.15.0 - created: "2025-12-17T14:48:33.826117296+01:00" + created: "2026-07-27T16:59:31.275355+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 002dd47647bf51fbba023bd1762d807be478cf37de7a44b80cd01ac1f20bd94a @@ -47,7 +69,7 @@ entries: version: 1.15.0 - apiVersion: v2 appVersion: 1.14.0 - created: "2025-12-17T14:48:33.819729144+01:00" + created: "2026-07-27T16:59:31.274572+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 36e1571f3f455b213f16cdda7b1158648e8e84deb804ba47ed6b9b6d19263ba8 @@ -69,7 +91,7 @@ entries: version: 1.14.0 - apiVersion: v2 appVersion: 1.13.0 - created: "2025-12-17T14:48:33.81038602+01:00" + created: "2026-07-27T16:59:31.273293+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: a839601689aea0a7e6bc0712a5244d435683cf3314c95794097ff08540e1dfef @@ -91,7 +113,7 @@ entries: version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2025-12-17T14:48:33.803256825+01:00" + created: "2026-07-27T16:59:31.272505+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 65858d14a40d7fd90c32bd9fc60021acc9555c161079f43a365c70171eaf21d8 @@ -113,7 +135,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2025-12-17T14:48:33.797369053+01:00" + created: "2026-07-27T16:59:31.271511+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 3914b5e117bda0834f05c9207f007e2ac372864cf6e86dcc2e1362bbe46c14d9 @@ -133,26 +155,4 @@ entries: urls: - postgres-operator-1.11.0.tgz version: 1.11.0 - - apiVersion: v2 - appVersion: 1.10.1 - created: "2025-12-17T14:48:33.791368349+01:00" - description: Postgres Operator creates and manages PostgreSQL clusters running - in Kubernetes - digest: cc3baa41753da92466223d0b334df27e79c882296577b404a8e9071411fcf19c - 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.10.1.tgz - version: 1.10.1 -generated: "2025-12-17T14:48:33.785159183+01:00" +generated: "2026-07-27T16:59:31.270324+02:00" diff --git a/charts/postgres-operator/postgres-operator-1.10.1.tgz b/charts/postgres-operator/postgres-operator-1.10.1.tgz deleted file mode 100644 index 5ecb27a5b96abb117b37ebbf2a26ea3d071ff703..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17424 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 T@cjP)00960IDEtT0ImT5Y=5`F diff --git a/charts/postgres-operator/postgres-operator-2.0.0.tgz b/charts/postgres-operator/postgres-operator-2.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..99a223b4bf1353802873468a1faa76411595940d GIT binary patch literal 42996 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 z zc70Y|5EIKnS?iX~jn>G>FXv98y4JPZzHYa+k*c8z8ps<9N@oe33=Jb$})2dd3r~aPz!8MxeF%baM zg^SAE7LX?uG&^NPPb9v>a;2dWL}2of`1sAoCmCuT#dU#5ret6`Y6C}Cu&*h9^g?dG^ zau6E1)VDn>$>a#6!PA@-pQJd&99OI+ptr`{l!-VcD#pPBb>7JO5S=q+A{7Ar9k0Np zF1xB!a1M!`W#mt;$Jxt+b%BjEF?Sm0r>B#?DE|DjWijpEd~(;-l(oqgz>q4omHdVw$mCY*0fPQyLWKSC)r$Rn6u zi`7nvB%JvI_sRzoT;glAWw|anXlepgw+P~E=Eg_)Zy3Cp?#Y~L&(=VarlCLytjfc# zf{hCKlJPRiy6(k=C1HWr&cr4AlN{_UN))?8eP8w7I&vT{r6{~2D9oU7+~Y)vLaOqx ziKE&Xf`@;Z_ew_jWhAN96!9FYl(z>r7Ryc2V8+l%_&%zRECrLADIYtDSk(A`O$2%`m=`py3RyZ?rzWnmk~`%ow|EXwXA8z5l#zu%cLCwfgZnq^FM+bqTYqpU>xSklPf){Z(bX zWH8uW+aBTVL3?e`naLR@f-`zy+u_tksIq&b5$h>57d9XCfC%ouV;6_eIe2y@ysJTm zjSV&dD;9}urR^exC}qVcZ?AK<3GvZf#dVsFY{eNC!Ao)*PVa(((pE=Y?H)_Hozc7* zqyTcNDRWVtvU>J9B3JgsCqyQBeX0sR&M`oMbN~_YsELDj41|hy1YSjHn6f+lPgz&c zv5#KCwz{{h#pHnR~s-oh(%Sd6ttxO=laJkdYwt zKpy@T{wmw}>v2~PQb~~7PD-%nF<@^b!4{+BY?Pe-2YMSv4pY-g)uBJuCKjfDE6(0h zeAdeMFWRC&PG2x{0rZjKAKY6}+N@Z|3P#{US+8FgxGSAls`#8$3^I@XbZyNcE2Cbv z)6aG5O^A{|#%SKMGWBYxm`dk>`SEwxSjnlFNuT|Ry)rd5Hu|f-YK){p)x-XCtfKUL zR>3Lh#;iHbnGoq~liZ-CU(KV1+B2UPEnXEZ+{!)9Dn!LY8H8;!Lc+|a{w$3Z>t?=8 z6)#$or_q{qLZzC{Hb^bZm6d&GVZ75RBCy+Vlbl)`u*6rM9HA3+`p&nWNS!T|XU-t% zYK=CmJmu~Xd!;{@ov*n3@fX}+c+MgD>_vz<#-1`IUtHIq6)aUeGWOd69PTXXS97!d z)!u31Gr;CkG)Syc;{*B|Vaetwg;*M!S@clz{Wr1LwS zXfmO8xj4*wWy?#Xh@)Gw|0MUiX0@`G+?e*(j~AjiQIrVK8PAnutZ-(9*%A%ub#Vdh zbJGS$bF=$ak<2m!ZaU=+FGYrS!h%Tr=>PhJO6>w)odSF{3V;Q7^n-*j| z>s=rxo*;Yiv3g+U36*%B#;vBX_6-lWrLS4P>*<982HTuS_cxxN0$mZo!JT>0I86s} zK$12>pY$Ep4u?D)R@A#3IH>5LHH+H|QG9-f3ryHs@db3J{ev1kXX>mP}V2- zdU>Oq=~dpz^oYaV2L54A>(tf_UP`CkS;5E(G4*h$|7tG7b3KVjbGCQ7&k0aJONWhG zW6x0t51ad&?DEt!1bggy#^Uhi$^cq$+~+gUWRvPX6JY&^_U8jHo``#Tx0tvd-V#of zUlkkOthlT`@pZG}-+Z+a*^P0-7Tv|(-REt`r3uGjEhJw)uPw23zWL2qh^T>ASvJArGi$-t?*josrhljue&+n_DXSz z0^x7cw{35lretsPx?9b8kj@J!ikeN7`GI|YJy`xVukc9&-}ShocvxT&Ij+gtR+j3S zPw3jLd4Rg(#3nL1iWR3+xbsl=xiUnj5G3#|m-J}=B=N(RaFf)2)nOMUPy6i+!rhk^ z>7F0?a-#Cwb(y6Vf*`Dn(!fkj>QWtEV;1f2Q8lZ=9`!6WC^KNnCg75M&(qK*@0okw zEC{*Jj3rI%o_&?>d3S|;Ohnl6JL$W!H&h=hicB5+q9T7RmgE}_Qn1FPe ze){_| z*Jes15-kYWv{%aTpxg5Ht?(hJ{1Ha0h$&JV%uV<7K^=d$Z@!49 zmw3H<9tQO|*}Fyv2==~}O?ZgL-UO;(bYY*rD@m(ui~L0F!tRMy~5(kOm3$7=NAF`4- z6s8p%P*Txvqm-soo+C$gQgrw+)?!j@bUV`G>goAYyo!+|1NWab<8;w*aJE@KUi)NT zV5yg(vxU)u+5m;>XGoMlB{<5(NB`cu5K+=k-g#A6Xd&_J#&@GU|45fbXz+#|?w=^m_wA`bQ@ zTMz5Ec@`Qlf#=4r%dp8}@Rv(mM$oAjg>yZd`CY%_EcH#6Nms<_XTR8hTNkh2VD?No zxEM)b7hjvCvz4D8jN>~!_q~%n`x={gl_`R|#u_df&mJrx{Z(#dL6^3dudNL>1*_Cv z9_(mzWb8*Xn%5n%@(mW03#$C)(AR_QNSC{F>`aKrxZ8;3?!&Z<0kjw3dqb^Zs}-#^ z0HXzZ7HO4Cpc18rtz z*Wa^VzVr}qD`u$V(76_3Cnt^{)zxmWFLwj&SkcC!vIe|AvC2rup41;KQb%Tb_R%1f z_wiTWw1S>g8om(p!$s{TZND>`f^?rR3qtmc+|VPKNGwGQ*(#eh+rivzQcT;-oKSZV_SnBuEroyFa1 zmy=}WY0WBrqoxTiBoyZ`Gp_XqG}Riax>c6|nQf%}c#4*ET~*;y(1`b`QD<=9ZhBsE z?uLiDmLbIrFPZ{&GzzY=H6`zP86I%ZM=5)Gay`*S+b3!+g?ooR4lfTfUXX@Q`h;4i zZ+3n~+1b7D7HGErk$HGIDG;yt@RuR|51>;dqRdVHU_KD)B`{xe@nJRh^L_`Vs8$$f z_(TZJZ}+n#Tyxd?A7rai(^~U?NUjK+1CSg-zMsdyl!X~h;~o>s1ZA)g{ln93+l8D` zn8#lwn(iH5PG89x-#ZyRx|{14wTu5(BtE$R_HpZ)>w_r(g!4!6=v$tQ1f5;wZ!-Id zLAszRvq@5Hz?_<3UxWWNc3fm4FtJnf37bf3?3Q}aCLx{03BE-GnaRFIS{d@>sW(XD zgXfEyY-~?canHqAnID}tC+XHAlE1}4Wk1SJF+OV;cs^EqrZMsdJaT^x(e zMj1GXbcd-+EgNGigm1JcCn#^Ms&uiW*^RLq7<2L=-@C54@YskDBAx{Kcf_ECs` z*qVXr(OzC;Lyw(^iXK&%S~{#)BWn;R=q7XvrvD`2GP71F>t5)pvo$16P9Ch`$l>CY zK{P79e%y`u6z?`isb>~lN2Jm&Cz-l|PHj1rLG&QYYe%BSLuXHXUKi1=vtR0cQeM+M1oDmI_d+XlhVqz+9p5L`wt7_d4n@ z#FSs3|MlRDZTG}E-D3sVx*Q-2vEZC)d`&k(=w%3#8(L3*5BR<@<36zt18fjR^lm9c z=^s`IP%aNuD9}GJ?Qgky=?ZQvW6deG&InL$FL;qThHV?P*CwYWs{kH@TW2PsW)+54 zu@yk^&CHsXVc`IGMZsC&cH&4ZmOz;Z^J%ues+bfdD@X7RkrhQ#q8De ztG`Q4RtBv>di9iF${1f79_XKwwVN7Tl?+Gak!>m=E2V2zl{^; zPX6`gHOKGE2(&Snes=O%hyyXXG;O~=DzLx*a0-e^;cMN4=uxeESV56FiIl5>!O?nx z9LO=tD(Mj0Wt{_onFW|{M#&fFuHf&tEds?wOx4!MIASKI8Px>#vBEA-Gm*S#$-gcY z{?C8J3kCgO&z~M6wVZc1wJ;bkKp3NgG9b?5>(v5IDIj&-X?&w^63Tj`kHKHE+~5y3 za8P{QuLJ9e({Jraql&|r_wlX%wBi`-w`VsXW|t;FoY(XD+%NiB+miovaqvoTxIN;e zqxcQd3!0re!^ThMjXaz>$oA@nQL|Ir-mvdyDTe$pyH&grKqP3%DdL^jww4H>2yv`#j=wTE#yo zfPjy0Wrs-d@A~hj7=$X@zV3na*+@So>YexwFVJ&B8wlnB+XVgC14iP+6}V)Yt+-~1 zbb?N?w#&$Jv_{niUUp=O^Il@V9B4|h0r?PHmLZVlpI{|44 z1S+?Rv|g_VWim*Ld~mQpOs}Yw60Z&&dsso@anMG57u&Bl!WzrN5?p4{^_gM$(5?ll zQ#6@^rruqp^mA*o<-0~5o4E|6{sGFHB?yoWG}w4@Pv>RX zUssRN5q#T|V4q8C%-aJ~%%*@Z|9Ghf2-#TG#^Xz+4eerE9bv*2OCG7_hNIn@{G+ z)5zGgq=y=;7NH`MbX)z}4id4tgyY5yZh76AAzmlntr0V?e@G|~(uzmI=Goy+>q1(F zg4sswKvtG{WG-@7s{IM(=VM!}98qxNPXF00TVRu$zrTF;R#DYzRiF&Gt%uO!nQ;x# zn(EpEO6{u|Z?XEyiS?A*Cm)c6SkH>*O()=`pVL~r1R0rQ*z@@oGy(`J0|C@!jIp3p zyT7$a$b~PhVJKmcF~dRoz60`xqvt3P=|`0Mvzno<1J<108XCa1HE_-;`SZJ^++-3m zE^G#_tVWy9Tqz^LVqInc2NVN~&KyPEh%%iK2}<+_IPF?tL*A;htwx;=DoMLgBY7;N z;N*vg;At%eu%v6cl4D-Cxd$OUXfAA;H0@uw{g$prY3TH5cZ)`vZq-HHu`9COT5==z z;3jb@8hrRDw`g$V@Qx-#umsqVBN(fsa{TvA5=kq&FMdvEE>2&vbRN#Ljfx#Xl!eR9 zzCpO*t=j|Y#jLq!Dg}ve%Xf)uAGa*Rjn<;PiR?0Yh2lh+kzTVf-V8ch^&_5T>66RC zbZ(qN$xWNJrTKAVp%Qu}pn|#ld^t*gaLp;vQHI=AEvUk-#MO6v3Q(cH0Dvj?g+)4k z!%PR9XB;l6RE(Ce{ZA52gfPAVcwWm5=?m)~@&`}2g|`jFu#(x)0-U#^pbueH6Grz~ zgUJPg^6?jyWQPAtvM2MQkObB={YPfrhBLZ7;laFMh~oDNL5=M&K+v#cdy-d)LH<+j z+v6{y_3k`iHBJeZN}RIrg*qWyGLYE@Q@$}*G3oY4By}*{hOu3$L@E+`&#%6~C&6c9 zWzyLT5Kq^>K+gArAYZ=V0_LON#g`L5=y(LFwPDrVL4EJtf<0zgTmBL< zM04n?czZ+X9{huYaAgmI;9Xa~M_cY|>}2K4o^WymS?4NLAxS`6A>sqm0?TAuuTu8l zGO^;WUIvz7isf9%R*o6`b44oOzi7i5eO0d2?BqJ9H!$A2>xkC$-i)+ptT=o|mo|1y z=5ZBw@hjQ>HK&a2_p7?rm*BC!IJJxz4^NmQDqZx-WunAN3>7K--5NH~ zGjS;r zdF%u11AfvPYbSfKm)GF^=^WL$44BIFQnWDVepUkh1C*ECO}_%2hcd4ICpgT2fagHl3`LqfW4C_^VM9> z!$hKV!?pFDs)}E|jWwI;!749L6h{e0{V&^KoSN5;!Jj{OPP}+o@+7bfdB|BQ)&q!n zq<8Y-8+YN94@!MS(y<_p(?VQMfId;FRW_MvKru;c@U_aL{)D{MF;zUmp7kI3w34~7 z!HAq*q;k!#16VNuQ-r>BJGo*P2+ILB@?~h1%UGnbHx9}O+o()c36Md?O&!9nQAHNB zy{xd()Lmjzr0Fs5KdNl;ABBFH_7YD>ma`tn6Aq%UjTqxJp%il=_WB^Xg^6JsA8}-W z=`=joHNBpsC(6Bu9*}5+FW2j zy)GGCxV_s14Y;;H7@H)E&{t|B2Ah5`hAQZcHN9afn=QqRcNWs?0>J^i!YEtf<@iIP zy+YLT@@x#?pdmAgK(`P#2>1@jv4k1UxKiH@tLS_RlOSs#GTQ2KP}z34$P;!J+v@}U zMXe{wpAer%mu#4UG>RSuydhYJ)cF8%-IQpB@(?6AZ|ZOxBwNY#!~$ny+EQ+c!J&Z4 zDgf=dV_sjR%mj725TJzN@ElFJM^qL43cbsBHBdi~nza8bYjFbGbtDUgj1RAFc_ z@m>~gD<+lgI*+J&WsGBS#1w~D3frIjz5UDRJYffe6lN88%pFXfDMo;Nv5C!eYsg9 z4q~A{OZQURzPl>~{u{#9gCm!UWpMvw4RuS|JibssChC4#{L=6H=$fe zMvKNCT}@BRYfP^2EBf-aMw9BoiI<7-tGQGPMRrNduz#^jU-|B74sM&9R9nT?8BSCBYZWt z3225OB+>_V|r^Rg<99>@_JjPTi9#lV2${ z)1UVXNRq26?|sTx5b6c$c!Uktas+cd5^`VQByTnbgojdY7~0V-(Gm7yv?h;P!B+-P z0Q+Hy1dX%abMe2QW)RoMdu-2L%?-ajxY;kL=1Ax0Ek`wm%PjA@kW@qG6Oz`;57u0o z{0A_@5PF!B%vj&Jm4op=%|A^VRecFG@oIn1xzx%F+slp~l420TbW8;brId7aSLcT%7ww78NOC5Iu^R)1-Xkw#^=5V9tptyIOdZ zDIaoBb8nC;mmQ3azN^r#2+kwe{?bGD3Cgx>N$gZbh?Dnk*dOq~y%?_O&xtM6zB*sdWUBP|DvA)_G_*O%HO|K%oLcYN+)0 zHU@ISilSGAo~OSzr9;i-PraIr~s3oFPF z!8P4jZ^Nj0&4h&|H^nWC*ix>iS41zp<0(ol<(HMLt%{JTlk)(*JOHn z^XTUkSr?^-Ew6b94IPv#_)4kCy3)HO;_OU(+2dyTYQ>st`B7YpZ2#L!p?uadX~_Xb z7MnR8WbcRHq{PW5<7+V$orm)2I@NU1oZ-6#;9=vGKjyK4d$^R0Z$8q@U<)n?XU!Ul zOv%_?6$Z}b3EE+$8b5v$Zqbt@{~hn58b+r(H;sq**oB0Xj+5+tTR2+jdN0_;7Z z0phc|RL#an1}1fO3)yC`w+Q>pih1ol<$D#{m$C$?^Lxj6m%9|dXMA@9Ux{9`KvH5V z&rUO+FwI>|(#LLuVl!_alpK_L6cxApV%rmEn&MI= zRi8v7HcHhc6{@DnoOu;1+$!I>FsST~OlsVA2O`mTD3!cq8_O_8?TW1LJA|bZ-+~pT zzl4V_K*yWZ`v&grR^}u7O1F0|m<#4zJYZtNCfik#c#{Xm%GSgXD~0bwB}L63rv#E$Tjx6Fv%^(_NM5eS5Oy zuMJOi<>xlbO)$vB4RT9UbWrJ+>gx$mX=e#zC3e5x@juY+<@&lApg@UKDXqUnHd{Z9 zM&AJu;zw`wW41KL0u?V2LGR%SFH&eq^fXTNT(xd}x3cs=IU_3pJgka9sO2vwKR@Xe57w`-f+O+|j-)n}S?pYFOEG#WT3bLR`^5wkQ zmPRY_TSj11zYkLDw=S~ATWBaXrx?Y5T{=v6NNBM|@+Ap@P)ac0| z$}dvp6WhDRO=hI8d@(5O zO?L~hCwwU-eP1;Pa4se;w(enF7;92&<-of{9?K0r*R5}M9CKz>o*pv2>izw(}C{W>y)LZa^~ZK!I9Lj&~L~rO!8D! z{3SMG+AGAin6dspRpxW8x)m1`$HQyf#*(LlOT$2TUrg4K6S78E(Y^0pRl7n+(v|O9 zs4%97N;|oMB9`Gh`eZB6RtfZl%+V_%{IxuG_;p;|zOm0i*Mjkd$%xM$>;NoFE&C!?-@<*z zwO0&M+mA2QZ{#TG9gSE|>#z88nqa@4YsIIe5bEU}H(q4~QQYyhfVc(d#$)!|g^}sN zj9cmpm9Mgcp6=@uRR!aF5@qcsxLpnAj?#9E{B$-dzzS)U>i{H{Itg${Q6i_IxU3|mA4^@ zVIJ0XmP-UQv30h8UXKV;w_h(AIO+*Z!1|9G29lJw121DGgMr!HEi{>8Ff82Vd&o?- zL}`%mQZj>megW=AlOOxMnZr^)cinGsA={EaW;D6VwNUIVydebu8qK}yR0`iwTo(ry#R# zSlpT~h;=|cfLEIB>lr#)771%$P7hJCi}OY7(doSsr$uo%+bu!$PMcyQE=Un^uI)lY z`5}cL-~1M{ORWc}p%_QvlwaaEJX|z2u5Q2cmO`=qVO01i`1u8FEqb2)?m{(3)k=MGGoUp|fL^F06!)^t`3o6qGa&0X0fr=z+{8TgJn7-8xJ(Hz-}c4d=m3DQ8B`W* zqqSKuZ)Oh!g*onmXguqhRuUSjF-~2?ZoA6zf1I=j3F1f}{+wO|rtbvT=La!)##a8n zE`ZN3{XiBh#QJ6X*&2XCN;k%SQ4(JKiPuh{N%*}3P>xT=u?UpfK!_78hPK0hL90EG z3lVG-R|tc%PMw*^Di!4TWi`Xz)Na^k!@rU~ecLPFedGTz;qu?h$abwb7X@3yiz`<9 z(JK3GN67!hB)i%$pa3P>S&Y>~uN%mjg@*)HI%n8v0-(b6X$h!#59PB;XN66jie8>wkT;-ZQZ!xe`q&1%l;> zYWcozx^3V8aP4Vv`s4d|acbgE_}9CXgnt7$#NXVbm9hW38S?CdDyen@h?wq@HLHZd zP}5kft~L+Apw!IEJ5pkQgR)$Ar&v4Dbn1hE4yc)#;dxVB+Fua&APX7mGhlKa2tZ&p znI}-F)`^XYrhgSrPH{mATC+CHc%Q8A2MV(@9n?B>B*1Ed^=EI_*TZOjVJBmATqd~{ z3Ws4Le0v94QPs=U0)M#2uCyfPe+|Lpo)PrGJS3z`JsOuCn~{$WrNF*roDHOWKxbvw_HoG>^+0V^kIc59cxS1=-j*F7P4x;@IJHpqM+eJaM;rNWdI?nu z9iMhWd4B4Q`#gAF@0l2rjw`HCUvcuW#5#o)%Oo}_g#M)NxiMnD`qJO|Xa#6_)K<6m zkjGWYXn@-6x%WH?>h;@a-Y*plVJbaxnMM0Y`C3Iwz;i@ZuvI$!dLg93W_-7lAv>KE zSF|-x_m_rQhdc?|9%#=nokKfFS=P*e789e!*nIXDVWut&o+ch`#0gX|@sSNQwI`>< zoPNuBU>j}SaTSvy5l4Bv>V@VIFi3#V9AqHt;??It4R=K5-D23nK)>gc(M)|zfXtpd7nP;b zmfpAU@NGO+-~=KhE~krP*k_;^qs|{x+S|Ysf2PhA*@BI7v>1-gjLE)RC)iMvv2jAy ztE65{$8)d^^6sxpzFa$4DBGAkH1nUDXYLCqyeQlly1EblccX*zyb`N+Q`R@yGiRxp zt|X9!k~K;8Q9rXu-W+z0Kf~;J8LK_>2HRDzxSHR{N!*y0w?WTdw0K)t>M*(QZCLXK)K^iU{gzdh#xdC1*VQ#P=w3`3G{|{4SwY%=;^1N;P^`kq19lhVR4K>cUcZ zn}lb9rCh1DTy63Ri_|6*s*y>KynW(L@~ep27>b6;EUz>c~At z^4(xJ*_a)^k+C5SE5e^k_0(LYg$sCqa}a+B&T~$D>f&gCXtc&g6{tree{-0k}@Z_J2X}@GA4ylmiT2w~dRC#sdf4Dlxqo62xwO@C zkSHA>oGH@z7+WgOBbXaioh=rW|2Anfb2-N$a{jRP z{Hy%bO;F#qdMqT~s;F$*Uw14NJ%0Fo*B6}291MF6Zqe#~YV_=Ohi((L#2LjMzDnb% ztS}+#RZ)G{)969ACMWQ|fD||Rs_%OrHan#>;fV}M! z;DD|;Z%fG1kS@6T(R@5l`H|{tS1?VEkqgco#k7nQ{*P8#mI4JSY;&Mp@y1z-%dj|rP%_YvG#*vEDqDq0_V=$4`Nu|lRd3ah?~3xg9_T?Tj$Tq9 zZF*gH)5^Sb(`&M$E(n6Ax5kFOre)bIJy4M#1-VD7{7!Pbqb96+=ISbnm8Xvs&LS;^^rdlPQDR7koOlPjhR&OTV@a~BqH($DX>qdl?=Jme`IE$& zX@8BSEL7aL!7BEWj`}qX$*t;VG$OWkPXYW97%aa?Pznaau^#Zc!_@m9Ziw0#Q8;RP zt(6!-vea%X=`Xx7?C55vB0}+yDl3J-4xTP|?HU9t^9Dv$*Z{LkrKMe$^zELe3e_t7 zyGxg?IsUNYrOoD<8wTZB(=qJ7`h#MHCNpKl#H);}YF!N;O zxPMlGqcFGAc|rTJ;4;0Y!876s@thGGaU3r3fYeEI&&Csp3fT%Zmlr7`1VE*vn=)SJ zI7=y~xE*?0Ci;?oyQOZKn^v7zVB5{-1D0cSU%H!)5PFwgGp2j0sST9arjLM(--Cv5@*{nX#%xd6>6zz+E26juph9CQrtC zFWw)=)}Co{#j8{{56O%MAANw!EoR2iiq#d8jGShV>s-aK4>kkywU2WqAG-zuJsS8X z4uT@m(NMuZz3wk1YxlEJMbl6pL)1G>XcWU-U@Fll)DX>JCBc2-Y%NC9qJpA7dfZAx z1||ZR`QOP>xI`BzviZ|aFj=O`HR-rmS=SiPWZn;l5uK0o=NB_gI!zPi1VLT<@b%l= z{oVZq8q%>n=!2>M>&xe?Q(euruTyGz4VR>f=Gch(AXN}%QGO~2Cx73mnOZe@qIl96 zBy%Yr@_%@AC4^go$TzMjHI9g`nr&wfUnd@Kz*{hlGA)VMnDqSk8x7oLC4L2h+5m3q ztHN%Q`w7twr>eKN+jL#Op4XN zXVKDd2V-;qd5_{PowPDn(%on|^Jv^do;%rgs?Or6mxEsS{vA!Z?6X1fECQdB({K`% z{j9g!mvB8L5k?EAhUizhM|C2mt5%sWV|WNl;#s!M=ZKE4<6asU77A#4%-l3q`q{UE zNm$=zE?u`}CyEpO?d{0$^tl|5!a@eW_wdVk=x<`mJp8@ur`WDo;9#AMkvy$F;`?`m zjm~z1X3d+tex_5g&hN73wE3kdIhEud~3H%RV==ovREJ87U@w@1Q`R}#&&!WS+pH&swch~@SMoI^X(ED6%7C)$~pLHGT| zk7VO2&WWrV=Vq-2yf9qi{FH6(5pe)RiOmwv8g7KMT^||pOcTeEO52uJWF$4Baf(}V zBNj|%79csH*@+Ac5k2FeWdJg*azRhyI6ReYf;c5LpksM(%s84=Ie9icb@uI&dhHo^ zh;0vauP0J7++wOe_Y-wmF2ryU$tXWR)y`B)(MrJp2FGs1JgbL(Sm~$nLh~2#nSs-q zPd9i`CoG)$iQM2lFvJMZx^c zKKyS#N!UpSRB1+dpS`gxsW+HFcO@WfZs4_y3}fqF3zuDn4D!_E&nXPqYj+;=qZz6F zrs?Ti2dO2No{hHtP!2WZ>p0_v_RJ;Oq?*dfK#0z|Jw5Eu>}PT9sDn|t?c+D|&+e^8 z8kUizIB3Zc2G9U}leGQbglLn_{fRP)ukeg5O?FZZ%Zx<|+h3px=NA@}1sFU+QyAhB z(2iO6k`eayTn?t2yIxe3DHpzPh8?RV*{@#zH2YpmX{6#G=?hG(v?VRiSqz{3i*B)I zp1k9lGH-bGHYz+#kO2}$4Y2i-zL1t2=pj`binRa?8pt!MV?)3Avo3*_fyVWxrY06G zzIFR+OA}hq(F+Yhmj$<)OJ$#Vzt;brJkFs?U!-~Ww;$)WwL=WAeK&Tq88cxXHw2O)*p?cDu)eIY^Y*L;r-K*@sCWvx`pzJT07Ty>yHlHQ) zlTq2wB<+>OsVZ3l;1t1Y<5gmTDSoQM=Mu?GUSuH%$LAr( zN`;9sV-_P5Vy*ZLT50S5p0RpUrhsK~Xoco#Hd1fQX_YPuZhMJjgQHnkaGogKg~fl4 z_V#z#ftRw>ahmZV`FZCay4$I&Pr098s%vubaVq&0fqtXcvE$`{dc+sYlaaD_i&_Pf zKph)xxX2=Z<#Wkn+D_||^7;v}b-I#r8m-z9MWbv&+prf?|NIrhxCZA9arlSxY# z*)a}87Y?vQ(AW)^2kkT z(iH9TAZmx_)!!Xw>LX3Y{*n*dGhdx?`qr%mokYzx@yUl_7{8;ii8FmYhf(CZQ!v)~ zC?OYNHRYAL|JQuJI3cOlf~f(DQ&$)s_=NKOQHp$VZ0G-6Q=2#G-~Y6!^{?|c@%{V% zwLXD;@BjMa%HhKbjmY5L$p>!Dm5FeAd9fnJopx8Z3JJ}i~0-QQv4Ux54&L~GI z6UtdQ9R6RTsATbs!i%(h0h%2AE;V}fqj>JEu&e)uq5u9?asQ^-(thrT?9v?ceD?^! ze)gN#5qv|l9TI77H$qrt&bL~cc&6WA?OC-1FUcZ5k&r1ghL&sJe=DY8#}B1l%zPEg zfd`<`()b6lR}6nS<@UU9{I6S~%zOObzrE+1hxPs2|A&H(xVYydfMt*-9rOce)iCo~ zB`k%NO|k47Cm%vTRfNHhu`C_UTwLk@u?fvI-off@KI(2#<2!aD0|+W2!?O^uW`HE< z>jEL5Aq@ZF7jlFr1dvFPTRKJsamTQx*er7VRwwSwMefXC1u>c(TGNxX8bYtO6u5kzx8amP_Jw z=$0B~Y~vd9JGXJaRkQ7|WE*A6qfh&vukV~NM)8t z7us19Lwh6h=0(COjarFX>axl0L+%&amya$X1e z?>sf9kU1Hn16tdggQTf<@*XQg^q5%w$`Q>kjgP36BF#4XW5*FhR!{8kG`R))t(1ll zzklE}WZkl(Xd4~6@b}Pf)|XUQ&Q*6=Iqu?@Y-1Ynjj?8IO=d8_^%2xQ-uA4n)_NdP z#6e%$8OM`T{s?{n1ieDPK+dzB51N$OkfF~EMKJreK4*~ew7^)zT>=E;^XLFnbx^5Q zOsxfa3LA{bCPF6&kfinBMF_i}o@t30s1PF!WM%{M;}%}{-DAV-?oY~&5AS&FC_72A zexCYJuzxtq5gzB8*1XI>#<~oc?)@2-R4j*KZC7!v$C0TxzYP8%o}0#^z3M^H%#m7#aa#otVx{N z)g2VWK9z29tsjzuyLah~p9ctDM$2zd63R-GJC*Bw(k;?|ZnS6#YpVDBnkxU6zwrKR zs)#K|RW7a>RA}+n%JnXyk}+Wd0f#JV5ef4|#%|kr@{;q-sKnyJ560pz7PHoH1Fftf z``5#GuFR^ZJVfJ(_$WCcu60*!(ky@J9#guzOwl!4c4!v!MK@%Gqaz_6mQET;3eAam^QS`BZou0jI*1mkfK?{+ z3z^JYef;?g|EWY`O8&iyXr;AM7Vn~Bt}IA5Qvv<26s>P)<|s)1ETr*MxeJil`2ngW}Jr#-biQdhZ$hb?;9qeN$OC->4+MnY)3!R1&=}#PiOv&KtUC z!8s5HO;2V`F5?TDiq~+dEFN~Dw8+9?E%>!bvrF;nMSipBV2eyb%y{X;7hZSxo}7NhMEozW|pr6mi9&R`2yv(|FEJ?}_e-K(brNT3T*TlE7p~ zi16*v_?j;Sb^70iwLClRNJE{->GjCVP>23lMLBl5Q=bau3X(;_LgA?`%u0F&)=wTs zd6b47*@Xo)I#=Q^zeW{1Mtk1)v0;(h$RldaqIUdxByKn7Z0H(`o2rLF^J6$#!Xw~Q>}s2;PcMCg zjyCXg-}ebNvyL5)av}xOU}(Ty#v#C6igyI zkNNxKV=clyBM&7-d8`CT%8hecxT0yE7!Zmp+^2*=Nm}YEFBg+H4uv4iv1R1zl=m}W z(js=Iszv@h1cB5s!kjNhaJQu-9;%Y;8&W}2+F2aNmgUYOAG7hl^FBXkub`4I#QS#~ z7m*lsTYSj}*3w*BQ|@`ca7fxRTg2FzOLzIx<1@F?k@X^Y*+ zu3b*pBXJG!cHo9tgxyc`+awt;-FpKaHM-Xr8&?&c7`}YaW^-$uyQ#-Ux(nsr?>W0E z;CCN3Rc#3yHaw=AraNAAcu;V1;(Y2KuGHtslC>n`+pIc`I*r$4{HL3SCPs9}7VjLd z)aML;wgOw>+#IX7^`>E|w#6lrgCnmZ_|?G-4OVb`YFS_yoiA##BMLyaeH@cF6T6lh zH}u0J;()i(_+r?Se$F0(cKKf~epF{$OmdlJWRWoVTpUNwh(1)=l5sX}_gjphQ!?|J z>H88qSb-tRR{Yvg1T~%ddJRmFk#cd?rZ*50E(c)}?{*?<1Q};a_&u;wy+B$MR&6?G z&OHtjzWqYzCXDjXXOQ@AOGrG!ggW#NeMr|?=v|r^MKYF!=05FXECaP2t9bu=7{XvD zwijG(u0AeeIKkU|{2u9&4gd%rFcaJI79Ao|$&kk{RT147d&wg#K7Ydu(xy|_y*8xu z7<&pi(!jYkgR)wC6N3z^)JL%+_K7}y!{320Qw=r=L=?{P<|co8V(1R5^ug#d&tvx@ z?WvF+RWqZgJYdauUm(;=7<|SsnFU>xa)n?oH9wY~{DQSc>FW^k(ZZ=onWuR?nGKKR zBiy;qH&u`|a0;D~)O+4Jh9k;WN zX2=P~rib?9OJj&KrX9WqdqU|(F^f^|$aCaCc>ke;QF-~FLsjYBn^-}2db^O}Ns5Q+ zmgDR~6sU8N-62Hm>`Some905sFNI%EF}BF9I>r7k-at)O|T8ovLj>HK5KNgp}i|Ab;l6E+LHE zaJY}=*dg_Fmp4y@=9q>;*9L_Wg_B4W46bc-Qh)iv_|;!itk5gU zZ&vXbB!CMIJrJLfqKe$Zjy|y6z7o3OwT2FLhLTh zmko4|O|jYB$Z!uxEg2y!-vzA9$?^-sE#7nZ$Hw%+&V$Z{({_jr*pw!EIr;fho7+aeXLZ-=f(X5g3-K9!Q;yO{Po)C8v}uRvF8J2RB=3WLQ$s3n|AVV1-vgTG z@sre8k%>u5s$f=JaXna``U2e=ln<*Gk-We7O$chYF1M6N2fKse82jby+{@Y9*kgay zlBd$5K|c-7@!>`NuWxxj;oB`a=emTgV0i2O(RlJYBI`pmcr}5+w!=7u;_TtaJZag?bTLv!8<|w}trckC%U)dI;(4F)`7{q^h)N(@!VAKlbd?-7@Aop8tDN z>fw{u-Nknce1iXSx3}uH^nuP2uN_Z_yFzF6R(<-^Cd$K1EVkp|UuS}ZM*{x!4*Z}0 zZxf(3H>%VMSe1w(?64F*!htNV1r1|lF$RVJO=$U!BHZ8w%|9rj3?W>FcELBYTX|CA#&BDLV#Cwb K&t;ucLK6Tnzp42E diff --git a/docs/diagrams/operator.tex b/docs/diagrams/operator.tex deleted file mode 100644 index a8ee0a05f..000000000 --- a/docs/diagrams/operator.tex +++ /dev/null @@ -1,101 +0,0 @@ -\documentclass{article} -\usepackage{tikz} -\usepackage[graphics,tightpage,active]{preview} -\usetikzlibrary{arrows, shadows.blur, positioning, fit, calc, backgrounds} -\usepackage{lscape} - -\pagenumbering{gobble} - -\PreviewEnvironment{tikzpicture} -\PreviewEnvironment{equation} -\PreviewEnvironment{equation*} -\newlength{\imagewidth} -\newlength{\imagescale} -\pagestyle{empty} -\thispagestyle{empty} - -\begin{document} -\begin{center} -\begin{tikzpicture}[ - scale=0.5,transform shape, - font=\sffamily, - every matrix/.style={ampersand replacement=\&,column sep=2cm,row sep=2cm}, - operator/.style={draw,solid,thick,circle,fill=red!20,inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - component/.style={draw,solid,thick,rounded corners,fill=yellow!20,inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - border/.style={draw,dashed,rounded corners,fill=gray!20,inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - pod/.style={draw,solid,thick,rounded corners,fill=blue!20, inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - service/.style={draw,solid,thick,rounded corners,fill=blue!20, inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - endpoint/.style={draw,solid,thick,rounded corners,fill=blue!20, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - secret/.style={draw,solid,thick,rounded corners,fill=blue!20, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - pvc/.style={draw,solid,thick,rounded corners,fill=blue!20, inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - label/.style={rectangle,inner sep=0,outer sep=0}, - to/.style={->,>=stealth',shorten >=1pt,semithick,font=\sffamily\footnotesize}, - every node/.style={align=center}] - - % Position the nodes using a matrix layout - - \matrix{ - \& \node[component] (crd) {CRD}; \\ - \& \node[operator] (operator) {Operator}; \\ - \path - node[service] (service-master) {Master} - node[label, right of=service-master] (service-middle) {} - node[label, below of=service-middle] (services-label) {Services} - node[service, right=.5cm of service-master] (service-replica) {Replica} - node[border, behind path, - fit=(service-master)(service-replica)(services-label) - ] (services) {}; - \& - \node[component] (sts) {Statefulset}; \& \node[component] (pdb) {Pod Disruption Budget}; \\ - \path - node[service] (master-endpoint) {Master} - node[service, right=.5cm of master-endpoint] (replica-endpoint) {Replica} - node[label, right of=master-endpoint] (endpoint-middle) {} - node[label, below of=endpoint-middle] (endpoint-label) {Endpoints} - node[border, behind path, - fit=(master-endpoint)(replica-endpoint)(endpoint-label) - ] (endpoints) {}; \& - \node[component] (pod-template) {Pod Template}; \& - \node[border] (secrets) { - \begin{tikzpicture}[] - \node[secret] (users-secret) at (0, 0) {Users}; - \node[secret] (robots-secret) at (2, 0) {Robots}; - \node[secret] (standby-secret) at (4, 0) {Standby}; - \end{tikzpicture} \\ - Secrets - }; \\ \& - \path - node[pod] (replica1-pod) {Replica} - node[pod, left=.5cm of replica1-pod] (master-pod) {Master} - node[pod, right=.5cm of replica1-pod] (replica2-pod) {Replica} - node[label, below of=replica1-pod] (pod-label) {Pods} - node[border, behind path, - fit=(master-pod)(replica1-pod)(replica2-pod)(pod-label) - ] (pods) {}; \\ \& - \path - node[pvc] (replica1-pvc) {Replica} - node[pvc, left=.5cm of replica1-pvc] (master-pvc) {Master} - node[pvc, right=.5cm of replica1-pvc] (replica2-pvc) {Replica} - node[label, below of=replica1-pvc] (pvc-label) {Persistent Volume Claims} - node[border, behind path, - fit=(master-pvc)(replica1-pvc)(replica2-pvc)(pvc-label) - ] (pvcs) {}; \& - \\ \& \\ - }; - - % Draw the arrows between the nodes and label them. - \draw[to] (crd) -- node[midway,above] {} node[midway,below] {} (operator); - \draw[to] (operator) -- node[midway,above] {} node[midway,below] {} (sts); - \draw[to] (operator) -- node[midway,above] {} node[midway,below] {} (secrets); - \draw[to] (operator) -| node[midway,above] {} node[midway,below] {} (pdb); - \draw[to] (service-master) -- node[midway,above] {} node[midway,below] {} (master-endpoint); - \draw[to] (service-replica) -- node[midway,above] {} node[midway,below] {} (replica-endpoint); - \draw[to] (master-pod) -- node[midway,above] {} node[midway,below] {} (master-pvc); - \draw[to] (replica1-pod) -- node[midway,above] {} node[midway,below] {} (replica1-pvc); - \draw[to] (replica2-pod) -- node[midway,above] {} node[midway,below] {} (replica2-pvc); - \draw[to] (operator) -| node[midway,above] {} node[midway,below] {} (services); - \draw[to] (sts) -- node[midway,above] {} node[midway,below] {} (pod-template); - \draw[to] (pod-template) -- node[midway,above] {} node[midway,below] {} (pods); -\end{tikzpicture} -\end{center} -\end{document} diff --git a/docs/diagrams/pod.png b/docs/diagrams/pod.png deleted file mode 100644 index f54d1a2bdd684fd3c77242ca332e6278ea35ca1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241573 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�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π{PJ$TGR)x1Pk@G=ETU&6w`-6&b9|Vmc^aHe*)XA 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 diff --git a/docs/diagrams/pod.tex b/docs/diagrams/pod.tex deleted file mode 100644 index 291384642..000000000 --- a/docs/diagrams/pod.tex +++ /dev/null @@ -1,92 +0,0 @@ -\documentclass{article} -\usepackage{tikz} -\usepackage[graphics,tightpage,active]{preview} -\usetikzlibrary{arrows, shadows.blur, positioning, fit, calc, backgrounds} -\usepackage{lscape} - -\pagenumbering{gobble} - -\PreviewEnvironment{tikzpicture} -\PreviewEnvironment{equation} -\PreviewEnvironment{equation*} -\newlength{\imagewidth} -\newlength{\imagescale} -\pagestyle{empty} -\thispagestyle{empty} - -\begin{document} -\begin{center} -\begin{tikzpicture}[ - scale=0.5,transform shape, - font=\sffamily, - every matrix/.style={ampersand replacement=\&,column sep=2cm,row sep=2cm}, - pod/.style={draw,solid,thick,circle,fill=red!20,inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - component/.style={draw,solid,thick,rounded corners,fill=yellow!20,inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - border/.style={draw,dashed,rounded corners,fill=gray!20,inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - volume/.style={draw,solid,thick,rounded corners,fill=blue!20, inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - sidecar/.style={draw,solid,thick,rounded corners,fill=blue!20, inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - k8s-label/.style={draw,solid,thick,rounded corners,fill=blue!20, minimum width=1.5cm, inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - affinity/.style={draw,solid,thick,rounded corners,fill=blue!20, minimum width=2cm, inner sep=.3cm, blur shadow={shadow blur steps=5,shadow blur extra rounding=1.3pt}}, - label/.style={rectangle,inner sep=0,outer sep=0}, - to/.style={->,>=stealth',shorten >=1pt,semithick,font=\sffamily\footnotesize}, - every node/.style={align=center}] - - % Position the nodes using a matrix layout - - \matrix{ - \path - node[k8s-label] (app-label) {App} - node[k8s-label, right=.25cm of app-label] (role-label) {Role} - node[k8s-label, right=.25cm of role-label] (custom-label) {Custom} - node[label, below of=role-label] (k8s-label-label) {K8s Labels} - node[border, behind path, - fit=(app-label)(role-label)(custom-label)(k8s-label-label) - ] (k8s-labels) {}; \& \& - \path - node[affinity] (affinity) {Affinity} - node[label, right=.25cm of affinity] (affinity-middle) {} - node[affinity, right=.25cm of affinity-middle] (anti-affinity) {Anti-affinity} - node[label, below of=affinity-middle] (affinity-label) {Assigning to nodes} - node[border, behind path, - fit=(affinity)(anti-affinity)(affinity-label) - ] (affinity) {}; \\ - \& \node[pod] (pod) {Pod}; \& \\ - \path - node[volume, minimum width={width("shm-volume")}] (data-volume) {Data} - node[volume, right=.25cm of data-volume, minimum width={width("shm-volume")}] (tokens-volume) {Tokens} - node[volume, right=.25cm of tokens-volume] (shm-volume) {/dev/shm} - node[label, below of=tokens-volume] (volumes-label) {Volumes} - node[border, behind path, - fit=(data-volume)(shm-volume)(tokens-volume)(volumes-label) - ] (volumes) {}; \& - \node[component] (spilo) {Spilo}; \& - \node[sidecar] (scalyr) {Scalyr}; \& \\ \& - \path - node[component] (patroni) {Patroni} - node[component, below=.25cm of patroni] (postgres) {PostgreSQL} - node[border, behind path, - fit=(postgres)(patroni) - ] (spilo-components) {}; \& - \path - node[sidecar] (custom-sidecar1) {User defined} - node[label, right=.25cm of custom-sidecar1] (sidecars-middle) {} - node[sidecar, right=.25cm of sidecars-middle] (custom-sidecar2) {User defined} - node[label, below of=sidecars-middle] (sidecars-label) {Custom sidecars} - node[border, behind path, - fit=(custom-sidecar1)(custom-sidecar2)(sidecars-label) - ] (sidecars) {}; - \\ \& \\ - }; - - % Draw the arrows between the nodes and label them. - \draw[to] (pod) to [bend left=25] (volumes); - \draw[to] (pod) to [bend left=25] (k8s-labels); - \draw[to] (pod) to [bend right=25] (affinity); - \draw[to] (pod) to [bend right=25] (scalyr); - \draw[to] (pod) to [bend right=25] (sidecars); - \draw[to] (pod) -- node[midway,above] {} node[midway,below] {} (spilo); - \draw[to] (spilo) -- node[midway,above] {} node[midway,below] {} (spilo-components); - -\end{tikzpicture} -\end{center} -\end{document} diff --git a/docs/index.md b/docs/index.md index 1aeac0ccb..40626e25b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,15 +47,8 @@ flexibility to complement it with other tools like [ZMON](https://opensource.zal Here is a diagram, that summarizes what would be created by the operator, when a new Postgres cluster CRD is submitted: -![postgresql-operator](diagrams/operator.png "K8s resources, created by operator") - -This picture is not complete without an overview of what is inside a single -cluster pod, so let's zoom in: - -![pod](diagrams/pod.png "Database pod components") - -These two diagrams should help you to understand the basics of what kind of -functionality the operator provides. +![Features](diagrams/neutral_operator_dark.png#gh-dark-mode-only) +![Features](diagrams/neutral_operator_light.png#gh-light-mode-only) ## Status diff --git a/docs/migrate.md b/docs/migrate.md index e4ccd76b1..33aa847c6 100644 --- a/docs/migrate.md +++ b/docs/migrate.md @@ -2,6 +2,12 @@ Version 2.0 changes some default settings and removes deprecated fields. Please read the following sections before upgrading the Postgres Operator deployment. +## scram-sha-256 by default + +The new operator will default password encryption to `scram-sha-256`. Unless you configure `password_encryption: md5` in the manifest under `spec.postgresql.parameters` the operator will encrypt existing passwords in the secrets with `scram-sha-256` and alter the database passwords. Make sure that your used clients and drivers support `scram-sha-256` as pods will get rotated in rolling fashion after updating to Postgres Operator v2. + +The default Spilo image (`spilo-18:4.1-p2`) still configures the pg_hba.conf file to allow `md5` passwords but Postgres will validate `scram-sha-256` passwords correctly. Passwords of users that are not managed by the operator and are still `md5` encrypted need be altered before the next tagged Spilo image which will drop `md5` completely. + ## K8s Endpoints are deprecated If your current operator v1.x deployment is relying on K8s endpoints (the default setup) for Patroni to manage the HA state you have to start planning to switch to configmaps, because endpoints are deprecated from K8s 1.33 onwards. The default of the corresponding parameter `kubernetes_use_configmaps` is changing to `true` with v2.0 of the operator. This means you have to explicity set it to `false` in your configuration before you start the upgrade. @@ -16,7 +22,6 @@ We explicitly warn you to go straight to configmap-based HA management with data 4. The orphaned endpoints, which use the same names like the new configmaps, have to be deleted by you or your K8s garbage collection. - ## Dropped manifest fields We removed some deprecated fields from the Postgresql CRD. Please, make sure that you do not specify them in any of your cluster manifests. If you do, switch to the listed alternative: diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index caa1f6a1e..697ee3605 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -839,7 +839,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.15.1" + pipeline. Default: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" * **logical_backup_google_application_credentials** Specifies the path of the google cloud service account json file. Default is empty. diff --git a/e2e/run.sh b/e2e/run.sh index 0bcf67989..71d2c0f40 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -8,7 +8,7 @@ IFS=$'\n\t' readonly cluster_name="postgres-operator-e2e-tests" readonly kubeconfig_path="${HOME}/kind-config-${cluster_name}" -readonly spilo_image="ghcr.io/zalando/spilo-18:4.1-p1" +readonly spilo_image="ghcr.io/zalando/spilo-18:4.1-p2" readonly e2e_test_runner_image="ghcr.io/zalando/postgres-operator-e2e-tests-runner:latest" export GOPATH=${GOPATH-~/go} diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 8b64185e9..ca13966ed 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -14,7 +14,7 @@ SPILO_CURRENT = "ghcr.io/zalando/spilo-e2e:dev-18.3" SPILO_LAZY = "ghcr.io/zalando/spilo-e2e:dev-18.4" -SPILO_FULL_IMAGE = "ghcr.io/zalando/spilo-18:4.1-p1" +SPILO_FULL_IMAGE = "ghcr.io/zalando/spilo-18:4.1-p2" def to_selector(labels): return ",".join(["=".join(lbl) for lbl in labels.items()]) diff --git a/manifests/complete-postgres-manifest.yaml b/manifests/complete-postgres-manifest.yaml index 535f3a9f8..eff115cab 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-18:4.1-p1 + dockerImage: ghcr.io/zalando/spilo-18:4.1-p2 teamId: "acid" numberOfInstances: 2 users: # Application/Robot users diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index e85da7c48..7c8d2233f 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -34,7 +34,7 @@ data: default_memory_request: 100Mi # delete_annotation_date_key: delete-date # delete_annotation_name_key: delete-clustername - docker_image: ghcr.io/zalando/spilo-18:4.1-p1 + docker_image: ghcr.io/zalando/spilo-18:4.1-p2 # downscaler_annotations: "deployment-time,downscaler/*" enable_admin_role_for_users: "true" enable_crd_registration: "true" @@ -86,7 +86,7 @@ data: # logical_backup_cpu_limit: "" # logical_backup_cpu_request: "" logical_backup_cronjob_environment_secret: "" - logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" # logical_backup_google_application_credentials: "" logical_backup_job_prefix: "logical-backup-" # logical_backup_memory_limit: "" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 5fdf8aad8..68a934cf9 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -137,7 +137,7 @@ spec: type: boolean type: object docker_image: - default: ghcr.io/zalando/spilo-18:4.1-p1 + default: ghcr.io/zalando/spilo-18:4.1-p2 type: string enable_crd_registration: default: true @@ -644,7 +644,7 @@ spec: logical_backup_cronjob_environment_secret: type: string logical_backup_docker_image: - default: ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1 + default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0 type: string logical_backup_failed_jobs_history_limit: default: 3 diff --git a/manifests/postgres-operator.yaml b/manifests/postgres-operator.yaml index 253649078..80e4fa28b 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.15.1 + image: ghcr.io/zalando/postgres-operator:v2.0.0 imagePullPolicy: IfNotPresent resources: requests: diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 91f2b0888..28eba22db 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-18:4.1-p1 + docker_image: ghcr.io/zalando/spilo-18:4.1-p2 # enable_crd_registration: true # crd_categories: # - all @@ -182,7 +182,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.15.1" + logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" # logical_backup_google_application_credentials: "" logical_backup_job_prefix: "logical-backup-" logical_backup_provider: "s3" diff --git a/mkdocs.yml b/mkdocs.yml index b8e8c3e04..09fc841e6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ theme: readthedocs nav: - Concepts: 'index.md' - Quickstart: 'quickstart.md' + - Migration from v1: 'migrate.md' - Postgres Operator UI: 'operator-ui.md' - Admin guide: 'administrator.md' - User guide: 'user.md' 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 02765b8f1..920f379f4 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -352,7 +352,7 @@ type OperatorLogicalBackupConfiguration struct { // +kubebuilder:validation:Pattern=`^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$` // +kubebuilder:default="30 00 * * *" Schedule string `json:"logical_backup_schedule,omitempty"` - // +kubebuilder:default="ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + // +kubebuilder:default="ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" DockerImage string `json:"logical_backup_docker_image,omitempty"` // +kubebuilder:validation:Enum=az;gcs;s3 // +kubebuilder:default=s3 @@ -410,7 +410,7 @@ type OperatorConfigurationData struct { EtcdHost string `json:"etcd_host,omitempty"` // +kubebuilder:default=true KubernetesUseConfigMaps *bool `json:"kubernetes_use_configmaps,omitempty"` - // +kubebuilder:default="ghcr.io/zalando/spilo-18:4.1-p1" + // +kubebuilder:default="ghcr.io/zalando/spilo-18:4.1-p2" DockerImage string `json:"docker_image,omitempty"` // +kubebuilder:validation:Minimum=1 // +kubebuilder:default=8 diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 5fdf8aad8..68a934cf9 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -137,7 +137,7 @@ spec: type: boolean type: object docker_image: - default: ghcr.io/zalando/spilo-18:4.1-p1 + default: ghcr.io/zalando/spilo-18:4.1-p2 type: string enable_crd_registration: default: true @@ -644,7 +644,7 @@ spec: logical_backup_cronjob_environment_secret: type: string logical_backup_docker_image: - default: ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1 + default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0 type: string logical_backup_failed_jobs_history_limit: default: 3 diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 2db1223b4..94e237815 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -1692,8 +1692,8 @@ func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMoun func TestCompareLogicalBackupJob(t *testing.T) { - img1 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.14.0" - img2 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + img1 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + img2 := "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" clientSet := fake.NewSimpleClientset() acidClientSet := fakeacidv1.NewSimpleClientset() diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 0c237e8b4..136ca78e7 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -36,7 +36,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.EnableTeamIdClusternamePrefix = fromCRD.EnableTeamIdClusternamePrefix result.EtcdHost = fromCRD.EtcdHost result.KubernetesUseConfigMaps = util.CoalesceBool(fromCRD.KubernetesUseConfigMaps, util.True()) - result.DockerImage = util.Coalesce(fromCRD.DockerImage, "ghcr.io/zalando/spilo-18:4.1-p1") + result.DockerImage = util.Coalesce(fromCRD.DockerImage, "ghcr.io/zalando/spilo-18:4.1-p2") result.Workers = util.CoalesceUInt32(fromCRD.Workers, 8) result.MinInstances = fromCRD.MinInstances result.MaxInstances = fromCRD.MaxInstances @@ -184,7 +184,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur // logical backup config result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *") - result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1") + result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0") result.LogicalBackupProvider = util.Coalesce(fromCRD.LogicalBackup.BackupProvider, "s3") result.LogicalBackupAzureStorageAccountName = fromCRD.LogicalBackup.AzureStorageAccountName result.LogicalBackupAzureStorageAccountKey = fromCRD.LogicalBackup.AzureStorageAccountKey diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 538d44034..042c02d2b 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -128,7 +128,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.15.1"` + LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"ghcr.io/zalando/postgres-operator/logical-backup:v2.0.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:""` @@ -181,7 +181,7 @@ type Config struct { EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS EnableMaintenanceWindows *bool `name:"enable_maintenance_windows" default:"true"` MaintenanceWindows []string `name:"maintenance_windows"` - DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-18:4.1-p1"` + DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-18:4.1-p2"` 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"` diff --git a/ui/app/package.json b/ui/app/package.json index c5f1e61df..91aa478b0 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -1,6 +1,6 @@ { "name": "postgres-operator-ui", - "version": "1.15.1", + "version": "2.0.0", "description": "PostgreSQL Operator UI", "main": "src/app.js", "config": { diff --git a/ui/manifests/deployment.yaml b/ui/manifests/deployment.yaml index eb48d9605..5facce7c6 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.15.1 + image: ghcr.io/zalando/postgres-operator-ui:v2.0.0 ports: - containerPort: 8081 protocol: "TCP" From 29168ca0bc91d15f4fc0cc92cad5b8afcc593fb9 Mon Sep 17 00:00:00 2001 From: IO ANALYTICA <83664220+ioanalytica@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:02:59 +0200 Subject: [PATCH 60/70] Fix operatorconfigurations CRD: NamespacedName must render as type string (#3145) Upgrading to v2.0.0 fails at the CRD apply step. The apiserver rejects the OperatorConfiguration CRD: CustomResourceDefinition "operatorconfigurations.acid.zalan.do" is invalid: ...oauth_token_secret_name.default: Invalid value: "string": in body must be of type object Root cause: these CRDs are generated by controller-gen from the Go types. spec.NamespacedName is a struct {Namespace, Name}, so controller-gen emits an object schema for every NamespacedName field. But NamespacedName has custom MarshalJSON/UnmarshalJSON that (de)serialize it as a plain JSON string ("namespace/name"). The generated object schema therefore never matched how the operator actually reads and writes these fields (it did in 1.15.x, where they were type: string). For oauth_token_secret_name this is a hard failure: it also carries `// +kubebuilder:default=postgres-operator`, and a scalar string default on an object-typed property is rejected by the apiserver, blocking the whole 1.15.x -> 2.0.0 upgrade before the operator Deployment is touched. Fix at the source by annotating the NamespacedName type with `// +kubebuilder:validation:Type=string` and regenerating the CRDs. This makes controller-gen emit `type: string` for all NamespacedName fields (oauth_token_secret_name, infrastructure_roles_secret_name, pod_environment_configmap, and the nested infrastructure role secretname), matching their runtime serialization and restoring 1.15.x behaviour. A field-level Type override cannot be used here: it conflicts with the struct-derived schema ("object vs string in allOf") and fails generation. Regenerated manifests/operatorconfiguration.crd.yaml and its two synced copies (pkg/apis/... embed source and charts/.../crds). Verified with a server-side dry-run apply: the unpatched CRD is rejected, the regenerated CRD is accepted. Fixes #3143 --- .../crds/operatorconfigurations.yaml | 46 ++----------------- manifests/operatorconfiguration.crd.yaml | 46 ++----------------- .../v1/operatorconfiguration.crd.yaml | 46 ++----------------- pkg/spec/types.go | 5 ++ 4 files changed, 17 insertions(+), 126 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 68a934cf9..5084b72aa 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -226,24 +226,7 @@ spec: type: string type: array infrastructure_roles_secret_name: - description: |- - NamespacedName comprises a resource name, with a mandatory namespace, - rendered as "/". Being a type captures intent and - helps make sure that UIDs, namespaced names and non-namespaced names - do not get conflated in code. For most use cases, namespace and name - will already have been format validated at the API entry point, so we - don't do that here. Where that's not the case (e.g. in testing), - consider using NamespacedNameOrDie() in testing.go in this package. - - from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string infrastructure_roles_secrets: description: namespaced name of the secret containing infrastructure roles names and passwords @@ -265,14 +248,7 @@ spec: description: |- Name of a secret which describes the role, and optionally name of a configmap with an extra information - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string template: type: boolean userkey: @@ -458,14 +434,7 @@ spec: default: postgres-operator description: namespaced name of the secret containing the OAuth2 token to pass to the teams API - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string pdb_master_label_selector: default: true type: boolean @@ -485,14 +454,7 @@ spec: pod_environment_configmap: description: namespaced name of the ConfigMap with environment variables to populate on every pod - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string pod_environment_secret: type: string pod_management_policy: diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 68a934cf9..5084b72aa 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -226,24 +226,7 @@ spec: type: string type: array infrastructure_roles_secret_name: - description: |- - NamespacedName comprises a resource name, with a mandatory namespace, - rendered as "/". Being a type captures intent and - helps make sure that UIDs, namespaced names and non-namespaced names - do not get conflated in code. For most use cases, namespace and name - will already have been format validated at the API entry point, so we - don't do that here. Where that's not the case (e.g. in testing), - consider using NamespacedNameOrDie() in testing.go in this package. - - from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string infrastructure_roles_secrets: description: namespaced name of the secret containing infrastructure roles names and passwords @@ -265,14 +248,7 @@ spec: description: |- Name of a secret which describes the role, and optionally name of a configmap with an extra information - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string template: type: boolean userkey: @@ -458,14 +434,7 @@ spec: default: postgres-operator description: namespaced name of the secret containing the OAuth2 token to pass to the teams API - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string pdb_master_label_selector: default: true type: boolean @@ -485,14 +454,7 @@ spec: pod_environment_configmap: description: namespaced name of the ConfigMap with environment variables to populate on every pod - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string pod_environment_secret: type: string pod_management_policy: diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 68a934cf9..5084b72aa 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -226,24 +226,7 @@ spec: type: string type: array infrastructure_roles_secret_name: - description: |- - NamespacedName comprises a resource name, with a mandatory namespace, - rendered as "/". Being a type captures intent and - helps make sure that UIDs, namespaced names and non-namespaced names - do not get conflated in code. For most use cases, namespace and name - will already have been format validated at the API entry point, so we - don't do that here. Where that's not the case (e.g. in testing), - consider using NamespacedNameOrDie() in testing.go in this package. - - from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string infrastructure_roles_secrets: description: namespaced name of the secret containing infrastructure roles names and passwords @@ -265,14 +248,7 @@ spec: description: |- Name of a secret which describes the role, and optionally name of a configmap with an extra information - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string template: type: boolean userkey: @@ -458,14 +434,7 @@ spec: default: postgres-operator description: namespaced name of the secret containing the OAuth2 token to pass to the teams API - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string pdb_master_label_selector: default: true type: boolean @@ -485,14 +454,7 @@ spec: pod_environment_configmap: description: namespaced name of the ConfigMap with environment variables to populate on every pod - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object + type: string pod_environment_secret: type: string pod_management_policy: diff --git a/pkg/spec/types.go b/pkg/spec/types.go index 3f4abb70f..642f3fd9b 100644 --- a/pkg/spec/types.go +++ b/pkg/spec/types.go @@ -23,6 +23,11 @@ import ( // consider using NamespacedNameOrDie() in testing.go in this package. // // from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go +// +// NamespacedName marshals to and unmarshals from a plain JSON string +// ("/") via its custom MarshalJSON/UnmarshalJSON, so its CRD +// schema must be a string rather than the struct controller-gen would infer. +// +kubebuilder:validation:Type=string type NamespacedName struct { Namespace string `json:"namespace,omitempty"` Name string `json:"name"` From 66adeddcb73d692bead9cf178cb73336ed4ceeb2 Mon Sep 17 00:00:00 2001 From: IO ANALYTICA <83664220+ioanalytica@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:27:06 +0200 Subject: [PATCH 61/70] Fix invalid JSON (trailing comma) in operator-ui OPERATOR_UI_CONFIG (#3144) The postgresql_versions array had a trailing comma after "14" in two places that build OPERATOR_UI_CONFIG as JSON. OPERATOR_UI_CONFIG is parsed by the UI with Python's json.loads() (operator_ui/main.py), which rejects trailing commas, so the config fails to load: json.decoder.JSONDecodeError: Expecting value: line 25 column 3 Remove the trailing comma in: - charts/postgres-operator-ui/templates/deployment.yaml (chart Deployment) - ui/run_local.sh (local dev default_operator_ui_config) ui/manifests/deployment.yaml and the Python DEFAULT_UI_CONFIG in operator_ui/main.py were already correct. Fixes #3062 --- charts/postgres-operator-ui/templates/deployment.yaml | 2 +- ui/run_local.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/postgres-operator-ui/templates/deployment.yaml b/charts/postgres-operator-ui/templates/deployment.yaml index d4a2a88b8..a5729fcdc 100644 --- a/charts/postgres-operator-ui/templates/deployment.yaml +++ b/charts/postgres-operator-ui/templates/deployment.yaml @@ -88,7 +88,7 @@ spec: "17", "16", "15", - "14", + "14" ] } {{- if .Values.extraEnvs }} diff --git a/ui/run_local.sh b/ui/run_local.sh index 13d8f053a..917f1dc50 100755 --- a/ui/run_local.sh +++ b/ui/run_local.sh @@ -35,7 +35,7 @@ default_operator_ui_config='{ "17", "16", "15", - "14", + "14" ], "static_network_whitelist": { "localhost": ["172.0.0.1/32"] From dd230d631dba841779ef94c5597a2573982eee8e Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Tue, 28 Jul 2026 17:20:01 +0200 Subject: [PATCH 62/70] [helm] incl. latest bug fixes in v2 charts (#3146) --- charts/postgres-operator-ui/index.yaml | 16 ++++++++-------- .../postgres-operator-ui-2.0.0.tgz | Bin 5084 -> 5087 bytes charts/postgres-operator/index.yaml | 18 +++++++++--------- .../postgres-operator-2.0.0.tgz | Bin 42996 -> 42616 bytes 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/charts/postgres-operator-ui/index.yaml b/charts/postgres-operator-ui/index.yaml index fb01475ee..98bd89a9a 100644 --- a/charts/postgres-operator-ui/index.yaml +++ b/charts/postgres-operator-ui/index.yaml @@ -3,10 +3,10 @@ entries: postgres-operator-ui: - apiVersion: v2 appVersion: 2.0.0 - created: "2026-07-27T16:59:53.405736+02:00" + created: "2026-07-28T16:30:58.763571+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience - digest: 37d2392421822a6dfeed20f3ea40420c52e4e4aaf90354d20aa27175fbad6c29 + digest: 80098eb9290d77fcaaf813347386c3b7d34de0d4e846b50ac177d4ac4a9e2ecb home: https://github.com/zalando/postgres-operator keywords: - postgres @@ -26,7 +26,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: 1.15.1 - created: "2026-07-27T16:59:53.405452+02:00" + created: "2026-07-28T16:30:58.7632+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: 4bbb750934366038d692711f924151182b7be131b6822d011f5a4e51cf609482 @@ -49,7 +49,7 @@ entries: version: 1.15.1 - apiVersion: v2 appVersion: 1.14.0 - created: "2026-07-27T16:59:53.405196+02:00" + created: "2026-07-28T16:30:58.76293+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: e87ed898079a852957a67a4caf3fbd27b9098e413f5d961b7a771a6ae8b3e17c @@ -72,7 +72,7 @@ entries: version: 1.14.0 - apiVersion: v2 appVersion: 1.13.0 - created: "2026-07-27T16:59:53.404946+02:00" + created: "2026-07-28T16:30:58.762624+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: e0444e516b50f82002d1a733527813c51759a627cefdd1005cea73659f824ea8 @@ -95,7 +95,7 @@ entries: version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2026-07-27T16:59:53.404693+02:00" + created: "2026-07-28T16:30:58.762247+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: cbcef400c23ccece27d97369ad629278265c013e0a45c0b7f33e7568a082fedd @@ -118,7 +118,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2026-07-27T16:59:53.40439+02:00" + created: "2026-07-28T16:30:58.761377+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: a45f2284045c2a9a79750a36997386444f39b01ac722b17c84b431457577a3a2 @@ -139,4 +139,4 @@ entries: urls: - postgres-operator-ui-1.11.0.tgz version: 1.11.0 -generated: "2026-07-27T16:59:53.404012+02:00" +generated: "2026-07-28T16:30:58.760884+02:00" diff --git a/charts/postgres-operator-ui/postgres-operator-ui-2.0.0.tgz b/charts/postgres-operator-ui/postgres-operator-ui-2.0.0.tgz index 8faf3015576082ac8c1ae394c2a673dfca9a6526..66886b83a3376adcdccb005518e8406f8f1b4954 100644 GIT binary patch delta 5051 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 R{yP8w|Nm;=Jl6nl005>h@mBx< delta 5029 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 diff --git a/charts/postgres-operator/index.yaml b/charts/postgres-operator/index.yaml index 7f54a08c4..d85e6f3c0 100644 --- a/charts/postgres-operator/index.yaml +++ b/charts/postgres-operator/index.yaml @@ -3,10 +3,10 @@ entries: postgres-operator: - apiVersion: v2 appVersion: 2.0.0 - created: "2026-07-27T16:59:31.277476+02:00" + created: "2026-07-28T16:30:13.110262+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes - digest: 488ae36a71c075ebee4b759f52db31029d559cab20efb329cf151f206087deaf + digest: 2622899b573e4c46cd2a70677d5941a9e5da24c7db2a0ff774ddc49ad9d5c544 home: https://github.com/zalando/postgres-operator keywords: - postgres @@ -25,7 +25,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: 1.15.1 - created: "2026-07-27T16:59:31.276117+02:00" + created: "2026-07-28T16:30:13.108848+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 9f3edc3d796105c02c04eaae28a78e58fb08c1847a9de012245fd6ac2c0d2c00 @@ -47,7 +47,7 @@ entries: version: 1.15.1 - apiVersion: v2 appVersion: 1.15.0 - created: "2026-07-27T16:59:31.275355+02:00" + created: "2026-07-28T16:30:13.108147+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 002dd47647bf51fbba023bd1762d807be478cf37de7a44b80cd01ac1f20bd94a @@ -69,7 +69,7 @@ entries: version: 1.15.0 - apiVersion: v2 appVersion: 1.14.0 - created: "2026-07-27T16:59:31.274572+02:00" + created: "2026-07-28T16:30:13.107421+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 36e1571f3f455b213f16cdda7b1158648e8e84deb804ba47ed6b9b6d19263ba8 @@ -91,7 +91,7 @@ entries: version: 1.14.0 - apiVersion: v2 appVersion: 1.13.0 - created: "2026-07-27T16:59:31.273293+02:00" + created: "2026-07-28T16:30:13.106643+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: a839601689aea0a7e6bc0712a5244d435683cf3314c95794097ff08540e1dfef @@ -113,7 +113,7 @@ entries: version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2026-07-27T16:59:31.272505+02:00" + created: "2026-07-28T16:30:13.105204+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 65858d14a40d7fd90c32bd9fc60021acc9555c161079f43a365c70171eaf21d8 @@ -135,7 +135,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2026-07-27T16:59:31.271511+02:00" + created: "2026-07-28T16:30:13.104449+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 3914b5e117bda0834f05c9207f007e2ac372864cf6e86dcc2e1362bbe46c14d9 @@ -155,4 +155,4 @@ entries: urls: - postgres-operator-1.11.0.tgz version: 1.11.0 -generated: "2026-07-27T16:59:31.270324+02:00" +generated: "2026-07-28T16:30:13.103286+02:00" diff --git a/charts/postgres-operator/postgres-operator-2.0.0.tgz b/charts/postgres-operator/postgres-operator-2.0.0.tgz index 99a223b4bf1353802873468a1faa76411595940d..2b65213ce66e09ce08525afedb3ce80aec38cbe8 100644 GIT binary patch delta 41058 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 delta 41473 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 Date: Wed, 29 Jul 2026 10:51:00 +0200 Subject: [PATCH 63/70] prepare bugfix release (#3147) --- .../postgres-operator-issue-template.md | 2 +- README.md | 2 +- charts/postgres-operator-ui/Chart.yaml | 4 +- charts/postgres-operator-ui/index.yaml | 37 ++++++++++++++---- .../postgres-operator-ui-2.0.1.tgz | Bin 0 -> 5091 bytes charts/postgres-operator-ui/values.yaml | 2 +- charts/postgres-operator/Chart.yaml | 4 +- .../crds/operatorconfigurations.yaml | 4 +- .../postgres-operator-2.0.1.tgz | Bin 0 -> 42633 bytes charts/postgres-operator/values.yaml | 6 +-- docs/administrator.md | 2 +- docs/reference/operator_parameters.md | 4 +- manifests/configmap.yaml | 4 +- manifests/minimal-fake-pooler-deployment.yaml | 2 +- manifests/operatorconfiguration.crd.yaml | 4 +- manifests/postgres-operator.yaml | 2 +- ...gresql-operator-default-configuration.yaml | 4 +- .../v1/operator_configuration_type.go | 4 +- .../v1/operatorconfiguration.crd.yaml | 4 +- pkg/cluster/cluster_test.go | 2 +- pkg/controller/operator_config.go | 4 +- pkg/util/config/config.go | 4 +- ui/app/package.json | 2 +- ui/manifests/deployment.yaml | 2 +- 24 files changed, 64 insertions(+), 41 deletions(-) create mode 100644 charts/postgres-operator-ui/postgres-operator-ui-2.0.1.tgz create mode 100644 charts/postgres-operator/postgres-operator-2.0.1.tgz diff --git a/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md b/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md index 8e676f799..18568899d 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:v2.0.0 +- **Which image of the operator are you using?** e.g. ghcr.io/zalando/postgres-operator:v2.0.1 - **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/README.md b/README.md index a61af4568..3454ec41c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ production for over five years. | Release | Postgres versions | K8s versions | Golang | | :-------- | :---------------: | :---------------: | :-----: | -| v2.0.0 | 14 → 18 | 1.27+ | 1.26.4 | +| v2.0.1 | 14 → 18 | 1.27+ | 1.26.4 | | v1.15.1 | 13 → 17 | 1.27+ | 1.25.3 | | v1.14.0 | 13 → 17 | 1.27+ | 1.23.4 | | v1.13.0 | 12 → 16 | 1.27+ | 1.22.5 | diff --git a/charts/postgres-operator-ui/Chart.yaml b/charts/postgres-operator-ui/Chart.yaml index 7dba22103..759738933 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: 2.0.0 -appVersion: 2.0.0 +version: 2.0.1 +appVersion: 2.0.1 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 98bd89a9a..ff4456cbb 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: 2.0.1 + created: "2026-07-28T19:23:51.430413+02:00" + description: Postgres Operator UI provides a graphical interface for a convenient + database-as-a-service user experience + digest: 9bd0cfb82bc9e849fc01fe5a2d14e42941d102d719ff6a182a5a63237138c2a3 + 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-2.0.1.tgz + version: 2.0.1 - apiVersion: v2 appVersion: 2.0.0 - created: "2026-07-28T16:30:58.763571+02:00" + created: "2026-07-28T19:23:51.429462+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: 80098eb9290d77fcaaf813347386c3b7d34de0d4e846b50ac177d4ac4a9e2ecb @@ -26,7 +49,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: 1.15.1 - created: "2026-07-28T16:30:58.7632+02:00" + created: "2026-07-28T19:23:51.429214+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: 4bbb750934366038d692711f924151182b7be131b6822d011f5a4e51cf609482 @@ -49,7 +72,7 @@ entries: version: 1.15.1 - apiVersion: v2 appVersion: 1.14.0 - created: "2026-07-28T16:30:58.76293+02:00" + created: "2026-07-28T19:23:51.428962+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: e87ed898079a852957a67a4caf3fbd27b9098e413f5d961b7a771a6ae8b3e17c @@ -72,7 +95,7 @@ entries: version: 1.14.0 - apiVersion: v2 appVersion: 1.13.0 - created: "2026-07-28T16:30:58.762624+02:00" + created: "2026-07-28T19:23:51.428701+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: e0444e516b50f82002d1a733527813c51759a627cefdd1005cea73659f824ea8 @@ -95,7 +118,7 @@ entries: version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2026-07-28T16:30:58.762247+02:00" + created: "2026-07-28T19:23:51.428173+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: cbcef400c23ccece27d97369ad629278265c013e0a45c0b7f33e7568a082fedd @@ -118,7 +141,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2026-07-28T16:30:58.761377+02:00" + created: "2026-07-28T19:23:51.427892+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: a45f2284045c2a9a79750a36997386444f39b01ac722b17c84b431457577a3a2 @@ -139,4 +162,4 @@ entries: urls: - postgres-operator-ui-1.11.0.tgz version: 1.11.0 -generated: "2026-07-28T16:30:58.760884+02:00" +generated: "2026-07-28T19:23:51.42722+02:00" diff --git a/charts/postgres-operator-ui/postgres-operator-ui-2.0.1.tgz b/charts/postgres-operator-ui/postgres-operator-ui-2.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..fc12a17ff9192143029b78468bfdf8b26e3e41e4 GIT binary patch literal 5091 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

`fb(DVxY55-888FT>}Q8ouY}?$ymt_Z8pA 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 v!BGVkRDd-l49X%^gVn+Q>9GZ?=T///logical_backups`. The default image is the same image built with the Zalando-internal CI - pipeline. Default: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" + pipeline. Default: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1" * **logical_backup_google_application_credentials** Specifies the path of the google cloud service account json file. Default is empty. @@ -1080,7 +1080,7 @@ operator being able to provide some reasonable defaults. * **connection_pooler_image** Docker image to use for connection pooler deployment. - Default: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" + Default: "ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1" * **connection_pooler_max_db_connections** How many connections the pooler can max hold. This value is divided among the diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 7c8d2233f..b9628ebbc 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -17,7 +17,7 @@ data: connection_pooler_default_cpu_request: "500m" connection_pooler_default_memory_limit: 100Mi connection_pooler_default_memory_request: 100Mi - connection_pooler_image: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" + connection_pooler_image: "ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1" connection_pooler_max_db_connections: "60" connection_pooler_mode: "transaction" connection_pooler_number_of_instances: "2" @@ -86,7 +86,7 @@ data: # logical_backup_cpu_limit: "" # logical_backup_cpu_request: "" logical_backup_cronjob_environment_secret: "" - logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" + logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1" # logical_backup_google_application_credentials: "" logical_backup_job_prefix: "logical-backup-" # logical_backup_memory_limit: "" diff --git a/manifests/minimal-fake-pooler-deployment.yaml b/manifests/minimal-fake-pooler-deployment.yaml index 13f2cc68f..0de2dc57d 100644 --- a/manifests/minimal-fake-pooler-deployment.yaml +++ b/manifests/minimal-fake-pooler-deployment.yaml @@ -23,7 +23,7 @@ spec: serviceAccountName: postgres-operator containers: - name: postgres-operator - image: ghcr.io/zalando/postgres-operator/pgbouncer:latest + image: ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1 imagePullPolicy: IfNotPresent resources: requests: diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 5084b72aa..84d58cc8b 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -98,7 +98,7 @@ spec: pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string connection_pooler_image: - default: ghcr.io/zalando/postgres-operator/pgbouncer:latest + default: ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1 type: string connection_pooler_max_db_connections: format: int32 @@ -606,7 +606,7 @@ spec: logical_backup_cronjob_environment_secret: type: string logical_backup_docker_image: - default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0 + default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1 type: string logical_backup_failed_jobs_history_limit: default: 3 diff --git a/manifests/postgres-operator.yaml b/manifests/postgres-operator.yaml index 80e4fa28b..7e2705dea 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:v2.0.0 + image: ghcr.io/zalando/postgres-operator:v2.0.1 imagePullPolicy: IfNotPresent resources: requests: diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 28eba22db..80d848e6b 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -182,7 +182,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:v2.0.0" + logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1" # logical_backup_google_application_credentials: "" logical_backup_job_prefix: "logical-backup-" logical_backup_provider: "s3" @@ -227,7 +227,7 @@ configuration: connection_pooler_default_cpu_request: "500m" connection_pooler_default_memory_limit: 100Mi connection_pooler_default_memory_request: 100Mi - connection_pooler_image: "ghcr.io/zalando/postgres-operator/pgbouncer:latest" + connection_pooler_image: "ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1" # connection_pooler_max_db_connections: 60 connection_pooler_mode: "transaction" connection_pooler_number_of_instances: 2 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 920f379f4..d14ffc26e 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -331,7 +331,7 @@ type ConnectionPoolerConfiguration struct { Schema string `json:"connection_pooler_schema,omitempty"` // +kubebuilder:default=pooler User string `json:"connection_pooler_user,omitempty"` - // +kubebuilder:default="ghcr.io/zalando/postgres-operator/pgbouncer:latest" + // +kubebuilder:default="ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1" Image string `json:"connection_pooler_image,omitempty"` // +kubebuilder:validation:Enum=session;transaction // +kubebuilder:default=transaction @@ -352,7 +352,7 @@ type OperatorLogicalBackupConfiguration struct { // +kubebuilder:validation:Pattern=`^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$` // +kubebuilder:default="30 00 * * *" Schedule string `json:"logical_backup_schedule,omitempty"` - // +kubebuilder:default="ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" + // +kubebuilder:default="ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1" DockerImage string `json:"logical_backup_docker_image,omitempty"` // +kubebuilder:validation:Enum=az;gcs;s3 // +kubebuilder:default=s3 diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 5084b72aa..84d58cc8b 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -98,7 +98,7 @@ spec: pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string connection_pooler_image: - default: ghcr.io/zalando/postgres-operator/pgbouncer:latest + default: ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1 type: string connection_pooler_max_db_connections: format: int32 @@ -606,7 +606,7 @@ spec: logical_backup_cronjob_environment_secret: type: string logical_backup_docker_image: - default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0 + default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1 type: string logical_backup_failed_jobs_history_limit: default: 3 diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 94e237815..d651bea12 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -1693,7 +1693,7 @@ func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMoun func TestCompareLogicalBackupJob(t *testing.T) { img1 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" - img2 := "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0" + img2 := "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1" clientSet := fake.NewSimpleClientset() acidClientSet := fakeacidv1.NewSimpleClientset() diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 136ca78e7..6ee4522d8 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -184,7 +184,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur // logical backup config result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *") - result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.0") + result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1") result.LogicalBackupProvider = util.Coalesce(fromCRD.LogicalBackup.BackupProvider, "s3") result.LogicalBackupAzureStorageAccountName = fromCRD.LogicalBackup.AzureStorageAccountName result.LogicalBackupAzureStorageAccountKey = fromCRD.LogicalBackup.AzureStorageAccountKey @@ -270,7 +270,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.ConnectionPooler.Image = util.Coalesce( fromCRD.ConnectionPooler.Image, - "ghcr.io/zalando/postgres-operator/pgbouncer:latest") + "ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1") result.ConnectionPooler.Mode = util.Coalesce( fromCRD.ConnectionPooler.Mode, diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 042c02d2b..749fdaa90 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -128,7 +128,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:v2.0.0"` + LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1"` 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:""` @@ -158,7 +158,7 @@ type ConnectionPooler struct { NumberOfInstances *int32 `name:"connection_pooler_number_of_instances" default:"2"` Schema string `name:"connection_pooler_schema" default:"pooler"` User string `name:"connection_pooler_user" default:"pooler"` - Image string `name:"connection_pooler_image" default:"ghcr.io/zalando/postgres-operator/pgbouncer:latest"` + Image string `name:"connection_pooler_image" default:"ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1"` Mode string `name:"connection_pooler_mode" default:"transaction"` MaxDBConnections *int32 `name:"connection_pooler_max_db_connections" default:"60"` ConnectionPoolerDefaultCPURequest string `name:"connection_pooler_default_cpu_request"` diff --git a/ui/app/package.json b/ui/app/package.json index 91aa478b0..16e760ae5 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -1,6 +1,6 @@ { "name": "postgres-operator-ui", - "version": "2.0.0", + "version": "2.0.1", "description": "PostgreSQL Operator UI", "main": "src/app.js", "config": { diff --git a/ui/manifests/deployment.yaml b/ui/manifests/deployment.yaml index 5facce7c6..b34ca5465 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:v2.0.0 + image: ghcr.io/zalando/postgres-operator-ui:v2.0.1 ports: - containerPort: 8081 protocol: "TCP" From a7aaad0a0b9517bb46077f2925eb80ae8ebe1a0c Mon Sep 17 00:00:00 2001 From: Felix Kunde Date: Thu, 30 Jul 2026 12:43:52 +0200 Subject: [PATCH 64/70] update helm chart index (#3148) --- charts/postgres-operator/index.yaml | 38 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/charts/postgres-operator/index.yaml b/charts/postgres-operator/index.yaml index d85e6f3c0..fc1b8b73b 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: 2.0.1 + created: "2026-07-30T05:31:19.462094+02:00" + description: Postgres Operator creates and manages PostgreSQL clusters running + in Kubernetes + digest: bb54c367441fe36cefc08d1c83e52e4baaea3522e32da14d40ea45eba4669ff1 + 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-2.0.1.tgz + version: 2.0.1 - apiVersion: v2 appVersion: 2.0.0 - created: "2026-07-28T16:30:13.110262+02:00" + created: "2026-07-30T05:31:19.460757+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 2622899b573e4c46cd2a70677d5941a9e5da24c7db2a0ff774ddc49ad9d5c544 @@ -25,7 +47,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: 1.15.1 - created: "2026-07-28T16:30:13.108848+02:00" + created: "2026-07-30T05:31:19.45921+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 9f3edc3d796105c02c04eaae28a78e58fb08c1847a9de012245fd6ac2c0d2c00 @@ -47,7 +69,7 @@ entries: version: 1.15.1 - apiVersion: v2 appVersion: 1.15.0 - created: "2026-07-28T16:30:13.108147+02:00" + created: "2026-07-30T05:31:19.458416+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 002dd47647bf51fbba023bd1762d807be478cf37de7a44b80cd01ac1f20bd94a @@ -69,7 +91,7 @@ entries: version: 1.15.0 - apiVersion: v2 appVersion: 1.14.0 - created: "2026-07-28T16:30:13.107421+02:00" + created: "2026-07-30T05:31:19.457596+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 36e1571f3f455b213f16cdda7b1158648e8e84deb804ba47ed6b9b6d19263ba8 @@ -91,7 +113,7 @@ entries: version: 1.14.0 - apiVersion: v2 appVersion: 1.13.0 - created: "2026-07-28T16:30:13.106643+02:00" + created: "2026-07-30T05:31:19.456373+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: a839601689aea0a7e6bc0712a5244d435683cf3314c95794097ff08540e1dfef @@ -113,7 +135,7 @@ entries: version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2026-07-28T16:30:13.105204+02:00" + created: "2026-07-30T05:31:19.45558+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 65858d14a40d7fd90c32bd9fc60021acc9555c161079f43a365c70171eaf21d8 @@ -135,7 +157,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2026-07-28T16:30:13.104449+02:00" + created: "2026-07-30T05:31:19.454725+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 3914b5e117bda0834f05c9207f007e2ac372864cf6e86dcc2e1362bbe46c14d9 @@ -155,4 +177,4 @@ entries: urls: - postgres-operator-1.11.0.tgz version: 1.11.0 -generated: "2026-07-28T16:30:13.103286+02:00" +generated: "2026-07-30T05:31:19.453397+02:00" From bd8e361f652000e4020572bf10f8672f9cb3db17 Mon Sep 17 00:00:00 2001 From: Jan Mussler Date: Tue, 11 Aug 2026 09:14:13 +0200 Subject: [PATCH 65/70] Add deployment strategy type 'Recreate' (#3164) Add deployment strategy type 'Recreate' --- charts/postgres-operator/templates/deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/charts/postgres-operator/templates/deployment.yaml b/charts/postgres-operator/templates/deployment.yaml index c2eb0ba23..5a87f0224 100644 --- a/charts/postgres-operator/templates/deployment.yaml +++ b/charts/postgres-operator/templates/deployment.yaml @@ -10,6 +10,8 @@ metadata: namespace: {{ .Release.Namespace }} spec: replicas: 1 + strategy: + type: "Recreate" selector: matchLabels: app.kubernetes.io/name: {{ template "postgres-operator.name" . }} From b43c7be1d0fd5c144bc245406f91ab11247675f3 Mon Sep 17 00:00:00 2001 From: rasoanaivo-r Date: Tue, 11 Aug 2026 17:10:05 +0200 Subject: [PATCH 66/70] fix operatorconfigurations CRD: render sidecars as array (#3160) configuration.sidecars was annotated with kubebuilder:validation:Type=object while SidecarContainers is a []v1.Container, so the generated schema rejected every list value and global sidecars could not be configured at all. Drop the hand-written Schemaless/Type=object markers and let controller-gen derive the schema from the Go type, the same way spec.initContainers is already handled in the Postgresql CRD. The field now renders as type: array with a full Container schema for its items. Fixes #3159 Co-authored-by: Claude Opus 5 (1M context) --- .../crds/operatorconfigurations.yaml | 1523 ++++++++++++++++- manifests/operatorconfiguration.crd.yaml | 1523 ++++++++++++++++- .../v1/operator_configuration_type.go | 4 +- .../v1/operatorconfiguration.crd.yaml | 1523 ++++++++++++++++- 4 files changed, 4564 insertions(+), 9 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 84d58cc8b..0f21f7cf4 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -779,8 +779,1527 @@ spec: type: string type: object sidecars: - type: object - x-kubernetes-preserve-unknown-fields: true + items: + description: A single application container that you want to run + within a pod. + properties: + args: + description: |- + Arguments to the entrypoint. + The container image's CMD is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + description: |- + Entrypoint array. Not executed within a shell. + The container image's ENTRYPOINT is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + description: |- + List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + description: |- + List of sources to populate environment variables in the container. + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple + sources, the value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will take precedence. + Cannot be updated. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + description: |- + Container image name. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + lifecycle: + description: |- + Actions that the management system should take in response to container lifecycle events. + Cannot be updated. + properties: + postStart: + description: |- + PostStart is called immediately after a container is created. If the handler fails, + the container is terminated and restarted according to its restart policy. + Other management of the container blocks until the hook completes. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: |- + PreStop is called immediately before a container is terminated due to an + API request or management event such as liveness/startup probe failure, + preemption, resource contention, etc. The handler is not called if the + container crashes or exits. The Pod's termination grace period countdown begins before the + PreStop hook is executed. Regardless of the outcome of the handler, the + container will eventually terminate within the Pod's termination grace + period (unless delayed by finalizers). Other management of the container blocks until the hook completes + or until the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string + type: object + livenessProbe: + description: |- + Periodic probe of container liveness. + Container will be restarted if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + name: + description: |- + Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: |- + List of ports to expose from the container. Not specifying a port here + DOES NOT prevent that port from being exposed. Any port which is + listening on the default "0.0.0.0" address inside a container will be + accessible from the network. + Modifying this array with strategic merge patch may corrupt the data. + For more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: |- + Periodic probe of container service readiness. + Container will be removed from service endpoints if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + resizePolicy: + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. + items: + description: ContainerResizePolicy represents resource resize + policy for the container. + properties: + resourceName: + description: |- + Name of the resource to which this resource resize policy applies. + Supported values: cpu, memory. + type: string + restartPolicy: + description: |- + Restart policy to apply when specified resource is resized. + If not specified, it defaults to NotRequired. + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + Compute Resources required by this container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restartPolicy: + description: |- + RestartPolicy defines the restart behavior of individual containers in a pod. + This overrides the pod-level restart policy. When this field is not specified, + the restart behavior is defined by the Pod's restart policy and the container type. + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: + this init container will be continually restarted on + exit until all regular containers have terminated. Once all regular + containers have completed, all init containers with restartPolicy "Always" + will be shut down. This lifecycle differs from normal init containers and + is often referred to as a "sidecar" container. Although this init + container still starts in the init container sequence, it does not wait + for the container to complete before proceeding to the next init + container. Instead, the next init container starts immediately after this + init container is started, or after any startupProbe has successfully + completed. + type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: |- + StartupProbe indicates that the Pod has successfully initialized. + If specified, no other probes are executed until this completes successfully. + If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. + This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, + when it might take a long time to load data or warm a cache, than during steady-state operation. + This cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + stdin: + description: |- + Whether this container should allocate a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will always result in EOF. + Default is false. + type: boolean + stdinOnce: + description: |- + Whether the container runtime should close the stdin channel after it has been opened by + a single attach. When stdin is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the + first client attaches to stdin, and then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container is restarted. If this + flag is false, a container processes that reads from stdin will never receive an EOF. + Default is false + type: boolean + terminationMessagePath: + description: |- + Optional: Path at which the file to which the container's termination message + will be written is mounted into the container's filesystem. + Message written is intended to be brief final status, such as an assertion failure message. + Will be truncated by the node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. + Defaults to /dev/termination-log. + Cannot be updated. + type: string + terminationMessagePolicy: + description: |- + Indicate how the termination message should be populated. File will use the contents of + terminationMessagePath to populate the container status message on both success and failure. + FallbackToLogsOnError will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever is smaller. + Defaults to File. + Cannot be updated. + type: string + tty: + description: |- + Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + description: |- + Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + description: |- + Container's working directory. + If not specified, the container runtime's default will be used, which + might be configured in the container image. + Cannot be updated. + type: string + required: + - name + type: object + nullable: true + type: array teams_api: description: TeamsAPIConfiguration defines the configuration of TeamsAPI properties: diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 84d58cc8b..0f21f7cf4 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -779,8 +779,1527 @@ spec: type: string type: object sidecars: - type: object - x-kubernetes-preserve-unknown-fields: true + items: + description: A single application container that you want to run + within a pod. + properties: + args: + description: |- + Arguments to the entrypoint. + The container image's CMD is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + description: |- + Entrypoint array. Not executed within a shell. + The container image's ENTRYPOINT is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + description: |- + List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + description: |- + List of sources to populate environment variables in the container. + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple + sources, the value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will take precedence. + Cannot be updated. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + description: |- + Container image name. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + lifecycle: + description: |- + Actions that the management system should take in response to container lifecycle events. + Cannot be updated. + properties: + postStart: + description: |- + PostStart is called immediately after a container is created. If the handler fails, + the container is terminated and restarted according to its restart policy. + Other management of the container blocks until the hook completes. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: |- + PreStop is called immediately before a container is terminated due to an + API request or management event such as liveness/startup probe failure, + preemption, resource contention, etc. The handler is not called if the + container crashes or exits. The Pod's termination grace period countdown begins before the + PreStop hook is executed. Regardless of the outcome of the handler, the + container will eventually terminate within the Pod's termination grace + period (unless delayed by finalizers). Other management of the container blocks until the hook completes + or until the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string + type: object + livenessProbe: + description: |- + Periodic probe of container liveness. + Container will be restarted if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + name: + description: |- + Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: |- + List of ports to expose from the container. Not specifying a port here + DOES NOT prevent that port from being exposed. Any port which is + listening on the default "0.0.0.0" address inside a container will be + accessible from the network. + Modifying this array with strategic merge patch may corrupt the data. + For more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: |- + Periodic probe of container service readiness. + Container will be removed from service endpoints if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + resizePolicy: + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. + items: + description: ContainerResizePolicy represents resource resize + policy for the container. + properties: + resourceName: + description: |- + Name of the resource to which this resource resize policy applies. + Supported values: cpu, memory. + type: string + restartPolicy: + description: |- + Restart policy to apply when specified resource is resized. + If not specified, it defaults to NotRequired. + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + Compute Resources required by this container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restartPolicy: + description: |- + RestartPolicy defines the restart behavior of individual containers in a pod. + This overrides the pod-level restart policy. When this field is not specified, + the restart behavior is defined by the Pod's restart policy and the container type. + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: + this init container will be continually restarted on + exit until all regular containers have terminated. Once all regular + containers have completed, all init containers with restartPolicy "Always" + will be shut down. This lifecycle differs from normal init containers and + is often referred to as a "sidecar" container. Although this init + container still starts in the init container sequence, it does not wait + for the container to complete before proceeding to the next init + container. Instead, the next init container starts immediately after this + init container is started, or after any startupProbe has successfully + completed. + type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: |- + StartupProbe indicates that the Pod has successfully initialized. + If specified, no other probes are executed until this completes successfully. + If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. + This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, + when it might take a long time to load data or warm a cache, than during steady-state operation. + This cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + stdin: + description: |- + Whether this container should allocate a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will always result in EOF. + Default is false. + type: boolean + stdinOnce: + description: |- + Whether the container runtime should close the stdin channel after it has been opened by + a single attach. When stdin is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the + first client attaches to stdin, and then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container is restarted. If this + flag is false, a container processes that reads from stdin will never receive an EOF. + Default is false + type: boolean + terminationMessagePath: + description: |- + Optional: Path at which the file to which the container's termination message + will be written is mounted into the container's filesystem. + Message written is intended to be brief final status, such as an assertion failure message. + Will be truncated by the node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. + Defaults to /dev/termination-log. + Cannot be updated. + type: string + terminationMessagePolicy: + description: |- + Indicate how the termination message should be populated. File will use the contents of + terminationMessagePath to populate the container status message on both success and failure. + FallbackToLogsOnError will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever is smaller. + Defaults to File. + Cannot be updated. + type: string + tty: + description: |- + Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + description: |- + Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + description: |- + Container's working directory. + If not specified, the container runtime's default will be used, which + might be configured in the container image. + Cannot be updated. + type: string + required: + - name + type: object + nullable: true + type: array teams_api: description: TeamsAPIConfiguration defines the configuration of TeamsAPI properties: 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 d14ffc26e..23db5b196 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -430,9 +430,7 @@ type OperatorConfigurationData struct { // +kubebuilder:default=true ShmVolume *bool `json:"enable_shm_volume,omitempty"` SidecarImages map[string]string `json:"sidecar_docker_images,omitempty"` // deprecated in favour of SidecarContainers - // +kubebuilder:validation:XPreserveUnknownFields - // +kubebuilder:validation:Type=object - // +kubebuilder:validation:Schemaless + // +nullable SidecarContainers []v1.Container `json:"sidecars,omitempty"` // +optional PostgresUsersConfiguration PostgresUsersConfiguration `json:"users"` diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 84d58cc8b..0f21f7cf4 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -779,8 +779,1527 @@ spec: type: string type: object sidecars: - type: object - x-kubernetes-preserve-unknown-fields: true + items: + description: A single application container that you want to run + within a pod. + properties: + args: + description: |- + Arguments to the entrypoint. + The container image's CMD is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + description: |- + Entrypoint array. Not executed within a shell. + The container image's ENTRYPOINT is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + description: |- + List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + description: |- + List of sources to populate environment variables in the container. + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple + sources, the value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will take precedence. + Cannot be updated. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + description: |- + Container image name. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + lifecycle: + description: |- + Actions that the management system should take in response to container lifecycle events. + Cannot be updated. + properties: + postStart: + description: |- + PostStart is called immediately after a container is created. If the handler fails, + the container is terminated and restarted according to its restart policy. + Other management of the container blocks until the hook completes. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: |- + PreStop is called immediately before a container is terminated due to an + API request or management event such as liveness/startup probe failure, + preemption, resource contention, etc. The handler is not called if the + container crashes or exits. The Pod's termination grace period countdown begins before the + PreStop hook is executed. Regardless of the outcome of the handler, the + container will eventually terminate within the Pod's termination grace + period (unless delayed by finalizers). Other management of the container blocks until the hook completes + or until the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string + type: object + livenessProbe: + description: |- + Periodic probe of container liveness. + Container will be restarted if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + name: + description: |- + Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: |- + List of ports to expose from the container. Not specifying a port here + DOES NOT prevent that port from being exposed. Any port which is + listening on the default "0.0.0.0" address inside a container will be + accessible from the network. + Modifying this array with strategic merge patch may corrupt the data. + For more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: |- + Periodic probe of container service readiness. + Container will be removed from service endpoints if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + resizePolicy: + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. + items: + description: ContainerResizePolicy represents resource resize + policy for the container. + properties: + resourceName: + description: |- + Name of the resource to which this resource resize policy applies. + Supported values: cpu, memory. + type: string + restartPolicy: + description: |- + Restart policy to apply when specified resource is resized. + If not specified, it defaults to NotRequired. + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + Compute Resources required by this container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restartPolicy: + description: |- + RestartPolicy defines the restart behavior of individual containers in a pod. + This overrides the pod-level restart policy. When this field is not specified, + the restart behavior is defined by the Pod's restart policy and the container type. + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: + this init container will be continually restarted on + exit until all regular containers have terminated. Once all regular + containers have completed, all init containers with restartPolicy "Always" + will be shut down. This lifecycle differs from normal init containers and + is often referred to as a "sidecar" container. Although this init + container still starts in the init container sequence, it does not wait + for the container to complete before proceeding to the next init + container. Instead, the next init container starts immediately after this + init container is started, or after any startupProbe has successfully + completed. + type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: |- + StartupProbe indicates that the Pod has successfully initialized. + If specified, no other probes are executed until this completes successfully. + If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. + This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, + when it might take a long time to load data or warm a cache, than during steady-state operation. + This cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + stdin: + description: |- + Whether this container should allocate a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will always result in EOF. + Default is false. + type: boolean + stdinOnce: + description: |- + Whether the container runtime should close the stdin channel after it has been opened by + a single attach. When stdin is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the + first client attaches to stdin, and then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container is restarted. If this + flag is false, a container processes that reads from stdin will never receive an EOF. + Default is false + type: boolean + terminationMessagePath: + description: |- + Optional: Path at which the file to which the container's termination message + will be written is mounted into the container's filesystem. + Message written is intended to be brief final status, such as an assertion failure message. + Will be truncated by the node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. + Defaults to /dev/termination-log. + Cannot be updated. + type: string + terminationMessagePolicy: + description: |- + Indicate how the termination message should be populated. File will use the contents of + terminationMessagePath to populate the container status message on both success and failure. + FallbackToLogsOnError will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever is smaller. + Defaults to File. + Cannot be updated. + type: string + tty: + description: |- + Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + description: |- + Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + description: |- + Container's working directory. + If not specified, the container runtime's default will be used, which + might be configured in the container image. + Cannot be updated. + type: string + required: + - name + type: object + nullable: true + type: array teams_api: description: TeamsAPIConfiguration defines the configuration of TeamsAPI properties: From bbc3eab7e01008139d9ad115f5cdd2607af943b5 Mon Sep 17 00:00:00 2001 From: adshin21 <45573407+adshin21@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:52:39 +0530 Subject: [PATCH 67/70] Skip owner references on user secrets when secret deletion is disabled (#3165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Skip owner references on user secrets when secret deletion is disabled Kubernetes garbage-collects owner-referenced secrets as soon as the owning Postgresql resource is deleted, regardless of the operator's own EnableSecretsDeletion check in Delete() (which only guards the operator's explicit deleteSecrets() call, not GC). This made enable_secrets_deletion=false ineffective whenever enable_owner_references was also enabled, since GC removed the credential secrets anyway. Now the generated secrets are not removed when enable_owner_references: true, enable_secrets_deletion: false. * Document skip-owner-refs on user secrets when deletion disabled - refresh inline comment in generateSingleUserSecret - extend enable_owner_references / enable_secrets_deletion docs in operator_parameters.md to describe the interaction - clarify in operator_parameters.md that the protection takes effect on the cluster's next sync after the setting is applied - add third exception in administrator.md "Owner References and Finalizers" - add TestGenerateSingleUserSecret_OwnerReferences covering all four flag combinations plus the cross-namespace cases --------- Co-authored-by: Serdar Dalgıç --- docs/administrator.md | 3 +- docs/reference/operator_parameters.md | 18 +++- pkg/cluster/k8sres.go | 10 +- pkg/cluster/k8sres_test.go | 128 ++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 6 deletions(-) diff --git a/docs/administrator.md b/docs/administrator.md index e9c1b88b9..289bef8fb 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -285,11 +285,12 @@ 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 +monitoring with GitOps tools and enable cascading deletes. There are three 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 +* User-credential secrets when [`enable_secrets_deletion`](reference/operator_parameters.md#enable_secrets_deletion) is `false`, so Kubernetes garbage collection does not cascade-delete them after the Postgresql resource is removed (the `enable_secrets_deletion` flag alone only suppresses the operator's own delete path, not K8s GC) 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 diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 0f8cb3d55..e0b1aea79 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -289,8 +289,12 @@ configuration they are grouped under the `kubernetes` key. * **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). + monitoring and enable cascading deletion. User-credential secrets are also + excluded from controller owner references whenever + [enable_secrets_deletion](#enable_secrets_deletion) is `false`, so that + Kubernetes garbage collection does not cascade-delete them when the + Postgresql resource is removed. 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 @@ -381,7 +385,15 @@ configuration they are grouped under the `kubernetes` key. * **enable_secrets_deletion** By default, the operator deletes secrets when removing the Postgres cluster - manifest. To keep secrets, set this option to `false`. The default is `true`. + manifest. To keep secrets, set this option to `false`. Note that this only + guards the operator's own deletion logic; Kubernetes garbage collection can + still remove user-credential secrets when + [enable_owner_references](#enable_owner_references) is `true` because the + Postgresql resource acts as a controller owner. To prevent that, the + operator skips the controller owner reference on user-credential secrets + whenever `enable_secrets_deletion` is `false`, so the two settings work + together. This protection takes effect on the cluster's next sync after + the setting is applied. The default is `true`. * **enable_persistent_volume_claim_deletion** By default, the operator deletes persistent volume claims when removing the diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 5af6ead45..86d14f17b 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -1931,9 +1931,15 @@ func (c *Cluster) generateSingleUserSecret(pgUser spec.PgUser) *v1.Secret { lbls = c.connectionPoolerLabels("", false).MatchLabels } - // if secret lives in another namespace we cannot set ownerReferences + // Skip a controller ownerReference on user-credential secrets when the + // operator is configured to keep them (enable_secrets_deletion=false); + // otherwise Kubernetes garbage collection would still cascade-delete them + // once the owning Postgresql CR is removed, defeating that setting. + // Cross-namespace secrets also have no ownerReference because K8s forbids + // cross-namespace ownerRefs by design. var ownerReferences []metav1.OwnerReference - if c.Config.OpConfig.EnableCrossNamespaceSecret && c.Postgresql.ObjectMeta.Namespace != pgUser.Namespace { + secretsDeletionDisabled := c.OpConfig.EnableSecretsDeletion != nil && !*c.OpConfig.EnableSecretsDeletion + if secretsDeletionDisabled || (c.Config.OpConfig.EnableCrossNamespaceSecret && c.Postgresql.ObjectMeta.Namespace != pgUser.Namespace) { ownerReferences = nil } else { ownerReferences = c.ownerReferences() diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index d7afd80cc..013b09c07 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -2829,6 +2829,134 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { } } +func TestGenerateSingleUserSecret_OwnerReferences(t *testing.T) { + testName := "Test generateSingleUserSecret owner references" + + newCluster := func(ownerRefs, secretsDeletion *bool, crossNamespaceSecret bool) *Cluster { + cfg := Config{ + OpConfig: config.Config{ + Resources: config.Resources{ + ClusterNameLabel: "cluster-name", + PodRoleLabel: "spilo-role", + EnableOwnerReferences: ownerRefs, + }, + EnableSecretsDeletion: secretsDeletion, + EnableCrossNamespaceSecret: crossNamespaceSecret, + }, + } + pg := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp-database", + Namespace: "myapp", + UID: types.UID("myapp-database-uid"), + }, + Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 1}, + } + return New(cfg, k8sutil.KubernetesClient{}, pg, logger, eventRecorder) + } + + newPgUser := func(namespace string) spec.PgUser { + return spec.PgUser{ + Name: "app_user", + Namespace: namespace, + Password: "secret", + } + } + + hasControllerOwnerRef := func(cluster *Cluster) func(*v1.Secret) error { + return func(secret *v1.Secret) error { + for _, ref := range secret.OwnerReferences { + if ref.UID == cluster.Postgresql.ObjectMeta.UID && + ref.Name == cluster.Postgresql.ObjectMeta.Name && + ref.Controller != nil && *ref.Controller { + return nil + } + } + return fmt.Errorf("expected a controller owner reference pointing at the Postgresql CR, got %#v", + secret.OwnerReferences) + } + } + + hasNoControllerOwnerRef := func(cluster *Cluster) func(*v1.Secret) error { + return func(secret *v1.Secret) error { + for _, ref := range secret.OwnerReferences { + if ref.UID == cluster.Postgresql.ObjectMeta.UID && ref.Controller != nil && *ref.Controller { + return fmt.Errorf("expected no controller owner reference, got %#v", secret.OwnerReferences) + } + } + return nil + } + } + + tests := []struct { + scenario string + cluster *Cluster + pgUser spec.PgUser + expectControllerOwner bool + }{ + { + scenario: "owner refs + secrets deletion enabled (default)", + cluster: newCluster(util.True(), util.True(), false), + pgUser: newPgUser("myapp"), + expectControllerOwner: true, + }, + { + scenario: "owner refs enabled, secrets deletion disabled (skip owner ref)", + cluster: newCluster(util.True(), util.False(), false), + pgUser: newPgUser("myapp"), + expectControllerOwner: false, + }, + { + scenario: "owner refs enabled, secrets deletion unset (default true)", + cluster: newCluster(util.True(), nil, false), + pgUser: newPgUser("myapp"), + expectControllerOwner: true, + }, + { + scenario: "owner refs disabled, secrets deletion enabled", + cluster: newCluster(util.False(), util.True(), false), + pgUser: newPgUser("myapp"), + expectControllerOwner: false, + }, + { + scenario: "owner refs disabled, secrets deletion disabled", + cluster: newCluster(util.False(), util.False(), false), + pgUser: newPgUser("myapp"), + expectControllerOwner: false, + }, + { + scenario: "cross-namespace secret, owner refs + secrets deletion enabled", + cluster: newCluster(util.True(), util.True(), true), + pgUser: newPgUser("other-ns"), + expectControllerOwner: false, + }, + { + scenario: "cross-namespace secret, owner refs enabled, secrets deletion disabled", + cluster: newCluster(util.True(), util.False(), true), + pgUser: newPgUser("other-ns"), + expectControllerOwner: false, + }, + } + + for _, tt := range tests { + secret := tt.cluster.generateSingleUserSecret(tt.pgUser) + if secret == nil { + t.Errorf("%s [%s]: expected a non-nil secret", testName, tt.scenario) + continue + } + + var check func(*v1.Secret) error + if tt.expectControllerOwner { + check = hasControllerOwnerRef(tt.cluster) + } else { + check = hasNoControllerOwnerRef(tt.cluster) + } + if err := check(secret); err != nil { + t.Errorf("%s [%s]: %+v", testName, tt.scenario, err) + } + } +} + func TestGenerateService(t *testing.T) { var spec acidv1.PostgresSpec var cluster *Cluster From 6143460c4e5a1c62bdb3bdbdfb2f5b2ab923cbde Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:25:43 +0200 Subject: [PATCH 68/70] Bump js-yaml from 4.3.0 to 4.3.1 in /ui/app (#3166) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/package.json b/ui/app/package.json index 16e760ae5..e0c8564d0 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -38,7 +38,7 @@ "brfs": "^2.0.2", "dedent-js": "1.0.1", "eslint": "^8.32.0", - "js-yaml": "4.3.0", + "js-yaml": "4.3.1", "pug": "^3.0.2", "rimraf": "^4.1.2", "riot": "^3.13.2", From cbfe5954e108cafd10cd098cf6bed36d7ccfa2a3 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Thu, 13 Aug 2026 11:38:58 +0530 Subject: [PATCH 69/70] SCF-861: Regenerate CRDs and deepcopy after upstream sync Re-run controller-gen v0.17.3 against the merged Go types to pick up the cybertec-owned fields (lifecycle.phase, pitr_backup_retention) and new upstream additions (previousNumberOfInstances, previousPoolerInstances, sidecar array rendering fix). Changes are formatting + generated content only: manifests/operatorconfiguration.crd.yaml pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml charts/postgres-operator/crds/operatorconfigurations.yaml + pitr_backup_retention field (cybertec) manifests/postgresql.crd.yaml pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml charts/postgres-operator/crds/postgresqls.yaml + lifecycle field with phase enum (cybertec) + previousNumberOfInstances / previousPoolerInstances (upstream) ~ field reordering to controller-gen v0.17.3 alphabetical output + metadata: controller-gen.kubebuilder.io/version, app.kubernetes.io/name label + YAML front-matter pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go + DeepCopy logic for PitrBackupRetention (*metav1.Duration) Schema semantics unchanged. CRD validation behaviour identical. --- .../crds/operatorconfigurations.yaml | 2 + .../postgres-operator/crds/postgresqls.yaml | 20 + manifests/operatorconfiguration.crd.yaml | 2587 ++++++++++++++--- manifests/postgresql.crd.yaml | 26 +- .../v1/operatorconfiguration.crd.yaml | 2 + pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml | 26 +- .../acid.zalan.do/v1/zz_generated.deepcopy.go | 5 + 7 files changed, 2218 insertions(+), 450 deletions(-) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 0f21f7cf4..d211090c6 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -708,6 +708,8 @@ spec: enable_patroni_failsafe_mode: type: boolean type: object + pitr_backup_retention: + type: string postgres_pod_resources: description: PostgresPodResourcesDefaults defines the spec of default resources diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index f3dcbab59..a92812019 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -2021,6 +2021,18 @@ spec: - name type: object type: array + lifecycle: + description: LifecycleSpec describes the lifecycle state of a Postgres + cluster. + properties: + phase: + description: LifecyclePhase describes the desired lifecycle state + of a Postgres cluster. + enum: + - "" + - stopped + type: string + type: object livenessProbe: description: |- Probe describes a health check to be performed against a container to determine whether it is @@ -3340,6 +3352,14 @@ spec: properties: PostgresClusterStatus: type: string + previousNumberOfInstances: + format: int32 + type: integer + previousPoolerInstances: + additionalProperties: + format: int32 + type: integer + type: object required: - PostgresClusterStatus type: object diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 9d8f136f3..d211090c6 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -1,542 +1,636 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + labels: + app.kubernetes.io/name: postgres-operator name: operatorconfigurations.acid.zalan.do spec: group: acid.zalan.do names: + categories: + - all kind: OperatorConfiguration listKind: OperatorConfigurationList plural: operatorconfigurations - singular: operatorconfiguration shortNames: - opconfig - categories: - - all + singular: operatorconfiguration scope: Namespaced versions: - - name: v1 - served: true - storage: true - subresources: - status: {} - additionalPrinterColumns: - - name: Image - type: string - description: Spilo image to be used for Pods + - additionalPrinterColumns: + - description: Spilo image to be used for Pods jsonPath: .configuration.docker_image - - name: Cluster-Label + name: Image type: string - description: Label for K8s resources created by operator + - description: Label for K8s resources created by operator jsonPath: .configuration.kubernetes.cluster_name_label - - name: Service-Account + name: Cluster-Label type: string - description: Name of service account to be used + - description: Name of service account to be used jsonPath: .configuration.kubernetes.pod_service_account_name - - name: Min-Instances - type: integer - description: Minimum number of instances per Postgres cluster + name: Service-Account + type: string + - description: Minimum number of instances per Postgres cluster jsonPath: .configuration.min_instances - - name: Age - type: date + name: Min-Instances + type: integer + - description: Age of the OperatorConfiguration resource jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: - type: object - required: - - kind - - apiVersion - - configuration + description: OperatorConfiguration defines the specification for the OperatorConfiguration. properties: - kind: - type: string - enum: - - OperatorConfiguration apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - enum: - - acid.zalan.do/v1 configuration: - type: object + description: OperatorConfigurationData defines the operation config properties: + aws_or_gcp: + description: AWSGCPConfiguration defines the configuration for AWS + properties: + additional_secret_mount: + type: string + additional_secret_mount_path: + type: string + aws_region: + default: eu-central-1 + type: string + gcp_credentials: + type: string + irsa_role_arn: + type: string + kube_iam_role: + type: string + log_s3_bucket: + type: string + wal_az_storage_account: + type: string + wal_gs_bucket: + type: string + wal_s3_bucket: + type: string + type: object + connection_pooler: + description: ConnectionPoolerConfiguration defines default configuration + for connection pooler + properties: + connection_pooler_default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + connection_pooler_default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + connection_pooler_image: + default: ghcr.io/zalando/postgres-operator/pgbouncer:v2.0.1 + type: string + connection_pooler_max_db_connections: + format: int32 + type: integer + connection_pooler_mode: + default: transaction + enum: + - session + - transaction + type: string + connection_pooler_number_of_instances: + default: 2 + format: int32 + minimum: 1 + type: integer + connection_pooler_schema: + default: pooler + type: string + connection_pooler_user: + default: pooler + type: string + type: object crd_categories: - type: array - nullable: true items: type: string + type: array + debug: + description: OperatorDebugConfiguration defines options for the debug + mode + properties: + debug_logging: + default: true + type: boolean + enable_database_access: + default: true + type: boolean + type: object docker_image: + default: ghcr.io/zalando/spilo-18:4.1-p2 type: string - default: "ghcr.io/zalando/spilo-18:4.1-p1" enable_crd_registration: - type: boolean default: true - enable_crd_validation: type: boolean - description: deprecated - default: true enable_lazy_spilo_upgrade: type: boolean - default: false - enable_pgversion_env_var: + enable_maintenance_windows: + default: true type: boolean + enable_pgversion_env_var: default: true - enable_shm_volume: type: boolean + enable_shm_volume: default: true + type: boolean enable_spilo_wal_path_compat: type: boolean - default: false enable_team_id_clustername_prefix: type: boolean - default: false etcd_host: - type: string default: "" + type: string ignore_instance_limits_annotation_key: type: string ignore_resources_limits_annotation_key: type: string - kubernetes_use_configmaps: - type: boolean - default: false - maintenance_windows: - items: - 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))\ *$' - type: string - type: array - max_instances: - type: integer - description: "-1 = disabled" - minimum: -1 - default: -1 - min_instances: - type: integer - description: "-1 = disabled" - minimum: -1 - default: -1 - resync_period: - type: string - default: "30m" - repair_period: - type: string - default: "5m" - pitr_backup_retention: - type: string - default: "168h" - set_memory_request_to_limit: - type: boolean - default: false - sidecar_docker_images: - type: object - additionalProperties: - type: string - sidecars: - type: array - nullable: true - items: - type: object - x-kubernetes-preserve-unknown-fields: true - workers: - type: integer - minimum: 1 - default: 8 - users: - type: object - properties: - additional_owner_roles: - type: array - nullable: true - items: - type: string - enable_password_rotation: - type: boolean - default: false - password_rotation_interval: - type: integer - default: 90 - password_rotation_user_retention: - type: integer - default: 180 - replication_username: - type: string - default: standby - super_username: - type: string - default: postgres - major_version_upgrade: - type: object - properties: - major_version_upgrade_mode: - type: string - default: "manual" - major_version_upgrade_team_allow_list: - type: array - items: - type: string - minimal_major_version: - type: string - default: "14" - target_major_version: - type: string - default: "18" kubernetes: - type: object + description: KubernetesMetaConfiguration defines k8s conf required + for all Postgres clusters and the operator itself properties: additional_pod_capabilities: - type: array items: type: string + type: array cluster_domain: + default: cluster.local type: string - default: "cluster.local" cluster_labels: - type: object additionalProperties: type: string default: application: spilo + type: object cluster_name_label: + default: cluster-name type: string - default: "cluster-name" custom_pod_annotations: - type: object additionalProperties: type: string + type: object delete_annotation_date_key: type: string delete_annotation_name_key: type: string downscaler_annotations: - type: array items: type: string + type: array enable_cross_namespace_secret: type: boolean - default: false enable_finalizers: type: boolean - default: false enable_init_containers: - type: boolean default: true + type: boolean enable_owner_references: type: boolean - default: false enable_persistent_volume_claim_deletion: - type: boolean default: true + type: boolean enable_pod_antiaffinity: type: boolean - default: false enable_pod_disruption_budget: - type: boolean default: true + type: boolean enable_readiness_probe: type: boolean - default: false enable_secrets_deletion: - type: boolean default: true - enable_sidecars: type: boolean + enable_sidecars: default: true + type: boolean ignored_annotations: - type: array items: type: string + type: array infrastructure_roles_secret_name: type: string infrastructure_roles_secrets: - type: array - nullable: true + description: namespaced name of the secret containing infrastructure + roles names and passwords items: - type: object - required: - - secretname - - userkey - - passwordkey properties: - secretname: + defaultrolevalue: type: string - userkey: + defaultuservalue: + type: string + details: + description: This field point out the detailed yaml definition + of the role, if exists type: string passwordkey: type: string rolekey: type: string - defaultuservalue: - type: string - defaultrolevalue: - type: string - details: + secretname: + description: |- + Name of a secret which describes the role, and optionally name of a + configmap with an extra information type: string template: type: boolean - inherited_annotations: + userkey: + type: string + type: object type: array + inherited_annotations: items: type: string - inherited_labels: type: array + inherited_labels: items: type: string + type: array + liveness_probe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object master_pod_move_timeout: + default: 20m + description: timeout for successful migration of master pods from + unschedulable node type: string - default: "20m" node_readiness_label: - type: object additionalProperties: type: string + type: object node_readiness_label_merge: - type: string enum: - - "AND" - - "OR" + - AND + - OR + type: string oauth_token_secret_name: + default: postgres-operator + description: namespaced name of the secret containing the OAuth2 + token to pass to the teams API type: string - default: "postgresql-operator" pdb_master_label_selector: - type: boolean default: true + type: boolean pdb_name_format: + default: postgres-{cluster}-pdb + description: defines the template for PDB names type: string - default: "postgres-{cluster}-pdb" persistent_volume_claim_retention_policy: + additionalProperties: + type: string type: object - properties: - when_deleted: - type: string - enum: - - "delete" - - "retain" - when_scaled: - type: string - enum: - - "delete" - - "retain" pod_antiaffinity_preferred_during_scheduling: type: boolean - default: false pod_antiaffinity_topology_key: + default: kubernetes.io/hostname type: string - default: "kubernetes.io/hostname" pod_environment_configmap: + description: namespaced name of the ConfigMap with environment + variables to populate on every pod type: string pod_environment_secret: type: string pod_management_policy: - type: string + default: ordered_ready enum: - - "ordered_ready" - - "parallel" - default: "ordered_ready" + - ordered_ready + - parallel + type: string pod_priority_class_name: type: string pod_role_label: + default: spilo-role type: string - default: "spilo-role" pod_service_account_definition: type: string - default: "" pod_service_account_name: + default: postgres-pod type: string - default: "postgres-pod" pod_service_account_role_binding_definition: type: string - default: "" pod_terminate_grace_period: + default: 5m + description: Postgres pods are terminated forcefully after this + timeout type: string - default: "5m" secret_name_template: + default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' + description: |- + template for database user secrets generated by the operator, + here username contains the namespace in the format namespace.username + if the user is in different namespace than cluster and cross namespace secrets + are enabled via `enable_cross_namespace_secret` flag in the configuration. type: string - default: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" share_pgsocket_with_sidecars: type: boolean - default: false spilo_allow_privilege_escalation: - type: boolean default: true - spilo_runasuser: - type: integer - spilo_runasgroup: - type: integer + type: boolean spilo_fsgroup: + format: int64 type: integer spilo_privileged: type: boolean - default: false + spilo_runasgroup: + format: int64 + type: integer + spilo_runasuser: + format: int64 + type: integer storage_resize_mode: - type: string + default: pvc enum: - - "ebs" - - "mixed" - - "pvc" - - "off" - default: "pvc" + - ebs + - mixed + - pvc + - "off" + type: string toleration: - type: object additionalProperties: type: string + type: object watched_namespace: type: string - postgres_pod_resources: type: object + kubernetes_use_configmaps: + default: true + type: boolean + load_balancer: + description: LoadBalancerConfiguration defines the LB configuration properties: - default_cpu_limit: + custom_service_annotations: + additionalProperties: + type: string + type: object + db_hosted_zone: type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - default_cpu_request: + enable_master_load_balancer: + type: boolean + enable_master_node_port: + type: boolean + enable_master_pooler_load_balancer: + type: boolean + enable_master_pooler_node_port: + type: boolean + enable_replica_load_balancer: + type: boolean + enable_replica_node_port: + type: boolean + enable_replica_pooler_load_balancer: + type: boolean + enable_replica_pooler_node_port: + type: boolean + external_traffic_policy: + default: Cluster + enum: + - Cluster + - Local type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - default_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - default_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - max_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - max_memory_request: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - min_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$|^$' - min_memory_limit: - type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$|^$' - timeouts: - type: object - properties: - patroni_api_check_interval: - type: string - default: "1s" - patroni_api_check_timeout: - type: string - default: "5s" - pod_label_wait_timeout: - type: string - default: "10m" - pod_deletion_wait_timeout: - type: string - default: "10m" - ready_wait_interval: - type: string - default: "4s" - ready_wait_timeout: - type: string - default: "30s" - resource_check_interval: - type: string - default: "3s" - resource_check_timeout: - type: string - default: "10m" - load_balancer: - type: object - properties: - custom_service_annotations: - type: object - additionalProperties: - type: string - db_hosted_zone: - type: string - default: "db.example.com" - enable_master_load_balancer: - type: boolean - default: true - enable_master_pooler_load_balancer: - type: boolean - default: false - enable_replica_load_balancer: - type: boolean - default: false - enable_replica_pooler_load_balancer: - type: boolean - default: false - external_traffic_policy: - type: string - enum: - - "Cluster" - - "Local" - default: "Cluster" master_dns_name_format: + default: '{cluster}.{namespace}.{hostedzone}' + description: defines the DNS name string template for the master + load balancer cluster type: string - default: "{cluster}.{namespace}.{hostedzone}" master_legacy_dns_name_format: + default: '{cluster}.{team}.{hostedzone}' + description: deprecated DNS template for master load balancer + using team name type: string - default: "{cluster}.{team}.{hostedzone}" replica_dns_name_format: + default: '{cluster}-repl.{namespace}.{hostedzone}' + description: defines the DNS name string template for the replica + load balancer cluster type: string - default: "{cluster}-repl.{namespace}.{hostedzone}" replica_legacy_dns_name_format: + default: '{cluster}-repl.{team}.{hostedzone}' + description: deprecated DNS template for replica load balancer + using team name type: string - default: "{cluster}-repl.{team}.{hostedzone}" - aws_or_gcp: type: object + logging_rest_api: + description: LoggingRESTAPIConfiguration defines Logging API conf properties: - additional_secret_mount: - type: string - additional_secret_mount_path: - type: string - aws_region: - type: string - default: "eu-central-1" - enable_ebs_gp3_migration: - type: boolean - default: false - enable_ebs_gp3_migration_max_size: + api_port: + default: 8080 type: integer + cluster_history_entries: default: 1000 - gcp_credentials: - type: string - kube_iam_role: - type: string - log_s3_bucket: - type: string - wal_az_storage_account: - type: string - wal_gs_bucket: - type: string - wal_s3_bucket: - type: string - logical_backup: + type: integer + ring_log_lines: + default: 100 + type: integer type: object + logical_backup: + description: OperatorLogicalBackupConfiguration defines configuration + for logical backup properties: + logical_backup_azure_storage_account_key: + type: string logical_backup_azure_storage_account_name: type: string logical_backup_azure_storage_container: type: string - logical_backup_azure_storage_account_key: - type: string logical_backup_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + logical_backup_cronjob_environment_secret: type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_docker_image: + default: ghcr.io/zalando/postgres-operator/logical-backup:v2.0.1 type: string - default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1" + logical_backup_failed_jobs_history_limit: + default: 3 + format: int32 + minimum: 0 + type: integer logical_backup_google_application_credentials: type: string logical_backup_job_prefix: + default: logical-backup- type: string - default: "logical-backup-" logical_backup_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' logical_backup_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' logical_backup_provider: - type: string + default: s3 enum: - - "az" - - "gcs" - - "s3" - default: "s3" + - az + - gcs + - s3 + type: string logical_backup_s3_access_key_id: type: string logical_backup_s3_bucket: @@ -547,162 +641,1807 @@ spec: type: string logical_backup_s3_region: type: string + logical_backup_s3_retention_time: + type: string logical_backup_s3_secret_access_key: type: string logical_backup_s3_sse: type: string - logical_backup_s3_retention_time: - type: string logical_backup_schedule: + default: 30 00 * * * + pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$ type: string - pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' - default: "30 00 * * *" - logical_backup_cronjob_environment_secret: + logical_backup_successful_jobs_history_limit: + default: 3 + format: int32 + minimum: 0 + type: integer + logical_backup_ttl_seconds_after_finished: + default: 86400 + format: int32 + minimum: 0 + type: integer + type: object + maintenance_windows: + items: + 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))\ + *$ + type: string + type: array + major_version_upgrade: + description: MajorVersionUpgradeConfiguration defines how to execute + major version upgrades of Postgres. + properties: + major_version_upgrade_mode: + default: manual + enum: + - "off" + - manual + - full + type: string + major_version_upgrade_team_allow_list: + items: + type: string + type: array + minimal_major_version: + default: "14" + type: string + target_major_version: + default: "18" type: string - debug: type: object + max_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + min_instances: + default: -1 + description: -1 = disabled + format: int32 + minimum: -1 + type: integer + patroni: + description: PatroniConfiguration defines configuration for Patroni properties: - debug_logging: - type: boolean - default: true - enable_database_access: + enable_patroni_failsafe_mode: type: boolean - default: true - teams_api: type: object + pitr_backup_retention: + type: string + postgres_pod_resources: + description: PostgresPodResourcesDefaults defines the spec of default + resources + properties: + default_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + default_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + default_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + max_cpu_request: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + max_memory_request: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + min_cpu_limit: + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + min_memory_limit: + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + type: object + repair_period: + default: 5m + description: period between consecutive repair requests + type: string + resync_period: + default: 30m + description: period between consecutive sync requests + type: string + scalyr: + description: ScalyrConfiguration defines the configuration for ScalyrAPI + properties: + scalyr_api_key: + type: string + scalyr_cpu_limit: + default: "1" + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_cpu_request: + default: 100m + pattern: ^(\d+m|\d+(\.\d{1,3})?)$ + type: string + scalyr_image: + type: string + scalyr_memory_limit: + default: 500Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_memory_request: + default: 50Mi + pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$ + type: string + scalyr_server_url: + default: https://upload.eu.scalyr.com + type: string + type: object + set_memory_request_to_limit: + type: boolean + sidecar_docker_images: + additionalProperties: + type: string + type: object + sidecars: + items: + description: A single application container that you want to run + within a pod. + properties: + args: + description: |- + Arguments to the entrypoint. + The container image's CMD is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + description: |- + Entrypoint array. Not executed within a shell. + The container image's ENTRYPOINT is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + of whether the variable exists or not. Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + description: |- + List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + description: |- + List of sources to populate environment variables in the container. + The keys defined within a source may consist of any printable ASCII characters except '='. + When a key exists in multiple + sources, the value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will take precedence. + Cannot be updated. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + description: |- + Container image name. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + lifecycle: + description: |- + Actions that the management system should take in response to container lifecycle events. + Cannot be updated. + properties: + postStart: + description: |- + PostStart is called immediately after a container is created. If the handler fails, + the container is terminated and restarted according to its restart policy. + Other management of the container blocks until the hook completes. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: |- + PreStop is called immediately before a container is terminated due to an + API request or management event such as liveness/startup probe failure, + preemption, resource contention, etc. The handler is not called if the + container crashes or exits. The Pod's termination grace period countdown begins before the + PreStop hook is executed. Regardless of the outcome of the handler, the + container will eventually terminate within the Pod's termination grace + period (unless delayed by finalizers). Other management of the container blocks until the hook completes + or until the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies a command to execute in + the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to + perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents a duration that the container + should sleep. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for backward compatibility. There is no validation of this field and + lifecycle hooks will fail at runtime when it is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + description: |- + StopSignal defines which signal will be sent to a container when it is being stopped. + If not specified, the default is defined by the container runtime in use. + StopSignal can only be set for Pods with a non-empty .spec.os.name + type: string + type: object + livenessProbe: + description: |- + Periodic probe of container liveness. + Container will be restarted if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + name: + description: |- + Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: |- + List of ports to expose from the container. Not specifying a port here + DOES NOT prevent that port from being exposed. Any port which is + listening on the default "0.0.0.0" address inside a container will be + accessible from the network. + Modifying this array with strategic merge patch may corrupt the data. + For more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: |- + Periodic probe of container service readiness. + Container will be removed from service endpoints if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + resizePolicy: + description: |- + Resources resize policy for the container. + This field cannot be set on ephemeral containers. + items: + description: ContainerResizePolicy represents resource resize + policy for the container. + properties: + resourceName: + description: |- + Name of the resource to which this resource resize policy applies. + Supported values: cpu, memory. + type: string + restartPolicy: + description: |- + Restart policy to apply when specified resource is resized. + If not specified, it defaults to NotRequired. + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + Compute Resources required by this container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restartPolicy: + description: |- + RestartPolicy defines the restart behavior of individual containers in a pod. + This overrides the pod-level restart policy. When this field is not specified, + the restart behavior is defined by the Pod's restart policy and the container type. + Additionally, setting the RestartPolicy as "Always" for the init container will + have the following effect: + this init container will be continually restarted on + exit until all regular containers have terminated. Once all regular + containers have completed, all init containers with restartPolicy "Always" + will be shut down. This lifecycle differs from normal init containers and + is often referred to as a "sidecar" container. Although this init + container still starts in the init container sequence, it does not wait + for the container to complete before proceeding to the next init + container. Instead, the next init container starts immediately after this + init container is started, or after any startupProbe has successfully + completed. + type: string + restartPolicyRules: + description: |- + Represents a list of rules to be checked to determine if the + container should be restarted on exit. The rules are evaluated in + order. Once a rule matches a container exit condition, the remaining + rules are ignored. If no rule matches the container exit condition, + the Container-level restart policy determines the whether the container + is restarted or not. Constraints on the rules: + - At most 20 rules are allowed. + - Rules can have the same action. + - Identical rules are not forbidden in validations. + When rules are specified, container MUST set RestartPolicy explicitly + even it if matches the Pod's RestartPolicy. + items: + description: ContainerRestartRule describes how a container + exit is handled. + properties: + action: + description: |- + Specifies the action taken on a container exit if the requirements + are satisfied. The only possible value is "Restart" to restart the + container. + type: string + exitCodes: + description: Represents the exit codes to check on container + exits. + properties: + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. Possible values are: + - In: the requirement is satisfied if the container exit code is in the + set of specified values. + - NotIn: the requirement is satisfied if the container exit code is + not in the set of specified values. + type: string + values: + description: |- + Specifies the set of values to check for container exit codes. + At most 255 elements are allowed. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: |- + StartupProbe indicates that the Pod has successfully initialized. + If specified, no other probes are executed until this completes successfully. + If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. + This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, + when it might take a long time to load data or warm a cache, than during steady-state operation. + This cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies a command to execute in the + container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + stdin: + description: |- + Whether this container should allocate a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will always result in EOF. + Default is false. + type: boolean + stdinOnce: + description: |- + Whether the container runtime should close the stdin channel after it has been opened by + a single attach. When stdin is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the + first client attaches to stdin, and then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container is restarted. If this + flag is false, a container processes that reads from stdin will never receive an EOF. + Default is false + type: boolean + terminationMessagePath: + description: |- + Optional: Path at which the file to which the container's termination message + will be written is mounted into the container's filesystem. + Message written is intended to be brief final status, such as an assertion failure message. + Will be truncated by the node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. + Defaults to /dev/termination-log. + Cannot be updated. + type: string + terminationMessagePolicy: + description: |- + Indicate how the termination message should be populated. File will use the contents of + terminationMessagePath to populate the container status message on both success and failure. + FallbackToLogsOnError will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever is smaller. + Defaults to File. + Cannot be updated. + type: string + tty: + description: |- + Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + description: |- + Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + description: |- + Container's working directory. + If not specified, the container runtime's default will be used, which + might be configured in the container image. + Cannot be updated. + type: string + required: + - name + type: object + nullable: true + type: array + teams_api: + description: TeamsAPIConfiguration defines the configuration of TeamsAPI properties: enable_admin_role_for_users: - type: boolean default: true - enable_postgres_team_crd: type: boolean + enable_postgres_team_crd: default: true + type: boolean enable_postgres_team_crd_superusers: type: boolean - default: false enable_team_member_deprecation: type: boolean - default: false enable_team_superuser: type: boolean - default: false enable_teams_api: type: boolean - default: true pam_configuration: + default: https://info.example.com/oauth2/tokeninfo?access_token= + uid realm=/employees type: string - default: "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees" pam_role_name: + default: zalandos type: string - default: "zalandos" postgres_superuser_teams: - type: array items: type: string - protected_role_names: type: array - items: - type: string + protected_role_names: default: - admin - cron_admin + items: + type: string + type: array role_deletion_suffix: + default: _deleted type: string - default: "_deleted" team_admin_role: + default: admin type: string - default: "admin" team_api_role_configuration: - type: object additionalProperties: type: string default: log_statement: all + type: object teams_api_url: + default: https://teams.example.com/api/ type: string - default: "https://teams.example.com/api/" - logging_rest_api: - type: object - properties: - api_port: - type: integer - default: 8080 - cluster_history_entries: - type: integer - default: 1000 - ring_log_lines: - type: integer - default: 100 - scalyr: # deprecated type: object + timeouts: + description: OperatorTimeouts defines the timeout of ResourceCheck, + PodWait, ReadyWait properties: - scalyr_api_key: + patroni_api_check_interval: + default: 1s + description: interval between consecutive attempts of operator + calling the Patroni API type: string - scalyr_cpu_limit: + patroni_api_check_timeout: + default: 5s + description: timeout when waiting for successful response from + Patroni API type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - default: "1" - scalyr_cpu_request: + pod_deletion_wait_timeout: + default: 10m + description: timeout when waiting for the Postgres pods to be + deleted type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - default: "100m" - scalyr_image: + pod_label_wait_timeout: + default: 10m + description: timeout when waiting for pod role and cluster labels type: string - scalyr_memory_limit: + ready_wait_interval: + default: 4s + description: interval between consecutive attempts waiting for + postgresql CRD to be created type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - default: "500Mi" - scalyr_memory_request: + ready_wait_timeout: + default: 30s + description: timeout for the complete postgres CRD creation type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - default: "50Mi" - scalyr_server_url: + resource_check_interval: + default: 3s + description: interval to wait between consecutive attempts to + check for some K8s resources + type: string + resource_check_timeout: + default: 10m + description: timeout when waiting for the presence of a certain + K8s resource type: string - default: "https://upload.eu.scalyr.com" - connection_pooler: type: object + users: + description: PostgresUsersConfiguration defines the system users of + Postgres. properties: - connection_pooler_schema: - type: string - default: "pooler" - connection_pooler_user: - type: string - default: "pooler" - connection_pooler_image: - type: string - default: "registry.opensource.zalan.do/acid/pgbouncer:master-32" - connection_pooler_max_db_connections: + additional_owner_roles: + items: + type: string + type: array + enable_password_rotation: + type: boolean + password_rotation_interval: + default: 90 + format: int32 type: integer - default: 60 - connection_pooler_mode: - type: string - enum: - - "session" - - "transaction" - default: "transaction" - connection_pooler_number_of_instances: + password_rotation_user_retention: + default: 120 + format: int32 type: integer - minimum: 1 - default: 2 - connection_pooler_default_cpu_limit: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - connection_pooler_default_cpu_request: - type: string - pattern: '^(\d+m|\d+(\.\d{1,3})?)$' - connection_pooler_default_memory_limit: + replication_username: + default: standby type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - connection_pooler_default_memory_request: + super_username: + default: postgres type: string - pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' - patroni: type: object - properties: - enable_patroni_failsafe_mode: - type: boolean - default: false - status: + workers: + default: 8 + format: int32 + minimum: 1 + type: integer type: object - additionalProperties: - type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + required: + - configuration + - metadata + type: object + served: true + storage: true + subresources: + status: {} diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index b98d00f71..a92812019 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -2021,6 +2021,18 @@ spec: - name type: object type: array + lifecycle: + description: LifecycleSpec describes the lifecycle state of a Postgres + cluster. + properties: + phase: + description: LifecyclePhase describes the desired lifecycle state + of a Postgres cluster. + enum: + - "" + - stopped + type: string + type: object livenessProbe: description: |- Probe describes a health check to be performed against a container to determine whether it is @@ -2174,18 +2186,6 @@ spec: format: int32 type: integer type: object - lifecycle: - description: LifecycleSpec describes the lifecycle state of a Postgres - cluster. - properties: - phase: - description: LifecyclePhase describes the desired lifecycle state - of a Postgres cluster. - enum: - - "" - - stopped - type: string - type: object logicalBackupRetention: type: string logicalBackupSchedule: @@ -3356,10 +3356,10 @@ spec: format: int32 type: integer previousPoolerInstances: - type: object additionalProperties: format: int32 type: integer + type: object required: - PostgresClusterStatus type: object diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 0f21f7cf4..d211090c6 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -708,6 +708,8 @@ spec: enable_patroni_failsafe_mode: type: boolean type: object + pitr_backup_retention: + type: string postgres_pod_resources: description: PostgresPodResourcesDefaults defines the spec of default resources diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index b98d00f71..a92812019 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -2021,6 +2021,18 @@ spec: - name type: object type: array + lifecycle: + description: LifecycleSpec describes the lifecycle state of a Postgres + cluster. + properties: + phase: + description: LifecyclePhase describes the desired lifecycle state + of a Postgres cluster. + enum: + - "" + - stopped + type: string + type: object livenessProbe: description: |- Probe describes a health check to be performed against a container to determine whether it is @@ -2174,18 +2186,6 @@ spec: format: int32 type: integer type: object - lifecycle: - description: LifecycleSpec describes the lifecycle state of a Postgres - cluster. - properties: - phase: - description: LifecyclePhase describes the desired lifecycle state - of a Postgres cluster. - enum: - - "" - - stopped - type: string - type: object logicalBackupRetention: type: string logicalBackupSchedule: @@ -3356,10 +3356,10 @@ spec: format: int32 type: integer previousPoolerInstances: - type: object additionalProperties: format: int32 type: integer + type: object required: - PostgresClusterStatus type: object 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 6ffc9c98b..4b9a2edd5 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -517,6 +517,11 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData in.LogicalBackup.DeepCopyInto(&out.LogicalBackup) in.ConnectionPooler.DeepCopyInto(&out.ConnectionPooler) in.Patroni.DeepCopyInto(&out.Patroni) + if in.PitrBackupRetention != nil { + in, out := &in.PitrBackupRetention, &out.PitrBackupRetention + *out = new(metav1.Duration) + **out = **in + } return } From e2e94c59a57788821d98d18d0e184fdc435cc82a Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Thu, 13 Aug 2026 12:13:00 +0530 Subject: [PATCH 70/70] SCF-861: Tidy CI workflows for cybertec fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - run_tests and run_e2e only fire on scalefield_v* PRs, with a manual trigger option - build-and-push runs on PRs to scalefield_v* and on any tag push, with unit tests gating the Harbor publish - publish_ghcr_image disabled — upstream-only workflow, not used by cybertec --- .github/workflows/build-and-push.yml | 15 ++++++++++----- .github/workflows/publish_ghcr_image.yaml | 2 +- .github/workflows/run_e2e.yaml | 4 ++-- .github/workflows/run_tests.yaml | 6 +++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 38a27ebc6..1125b44fe 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -8,11 +8,8 @@ on: branches: - 'scalefield_v*' push: - paths-ignore: - - '**.md' - - 'docs/**' - branches: - - 'scalefield_v*' + tags: + - '*' workflow_dispatch: inputs: image_tags: @@ -38,6 +35,14 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "^1.26.4" + + - name: Run unit tests + run: make mocks && go test -race ./... + - name: Determine tags id: tags run: | diff --git a/.github/workflows/publish_ghcr_image.yaml b/.github/workflows/publish_ghcr_image.yaml index 2fc22d0c9..4b3c6aa13 100644 --- a/.github/workflows/publish_ghcr_image.yaml +++ b/.github/workflows/publish_ghcr_image.yaml @@ -8,7 +8,7 @@ env: on: push: tags: - - '*' + - 'never-publish-scalefield-*' jobs: publish: diff --git a/.github/workflows/run_e2e.yaml b/.github/workflows/run_e2e.yaml index be99c5669..8c4cadf39 100644 --- a/.github/workflows/run_e2e.yaml +++ b/.github/workflows/run_e2e.yaml @@ -3,9 +3,9 @@ name: operator-e2e-tests on: workflow_call: pull_request: - push: branches: - - master + - scalefield_v* + workflow_dispatch: jobs: tests: diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 5f68f6309..d0fb77e9b 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -1,10 +1,10 @@ name: operator-tests -on: +on: pull_request: - push: branches: - - master + - scalefield_v* + workflow_dispatch: jobs: tests: