Merge branch 'master' into fix/set-password-encryption-default-to-scram-sha-256

This commit is contained in:
Felix Kunde
2026-06-03 18:17:28 +02:00
committed by GitHub
157 changed files with 17422 additions and 4076 deletions
+18 -14
View File
@@ -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"`
@@ -63,9 +64,10 @@ type Resources struct {
NodeReadinessLabelMerge string `name:"node_readiness_label_merge" default:"OR"`
ShmVolume *bool `name:"enable_shm_volume" default:"true"`
MaxInstances int32 `name:"max_instances" default:"-1"`
MinInstances int32 `name:"min_instances" default:"-1"`
IgnoreInstanceLimitsAnnotationKey string `name:"ignore_instance_limits_annotation_key"`
MaxInstances int32 `name:"max_instances" default:"-1"`
MinInstances int32 `name:"min_instances" default:"-1"`
IgnoreInstanceLimitsAnnotationKey string `name:"ignore_instance_limits_annotation_key"`
IgnoreResourcesLimitsAnnotationKey string `name:"ignore_resources_limits_annotation_key"`
}
type InfrastructureRole struct {
@@ -127,7 +129,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.14.0"`
LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"ghcr.io/zalando/postgres-operator/logical-backup:v1.15.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:""`
@@ -154,7 +156,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"`
@@ -172,13 +174,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
DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-17:4.0-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"`
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:""`
@@ -246,8 +250,8 @@ type Config struct {
EnableTeamIdClusternamePrefix bool `name:"enable_team_id_clustername_prefix" default:"false"`
MajorVersionUpgradeMode string `name:"major_version_upgrade_mode" default:"manual"`
MajorVersionUpgradeTeamAllowList []string `name:"major_version_upgrade_team_allow_list" default:""`
MinimalMajorVersion string `name:"minimal_major_version" default:"13"`
TargetMajorVersion string `name:"target_major_version" default:"17"`
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"`
EnablePatroniFailsafeMode *bool `name:"enable_patroni_failsafe_mode" default:"false"`
+306
View File
@@ -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)
}
})
}
}
-2
View File
@@ -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"
+1 -1
View File
@@ -1,6 +1,6 @@
package httpclient
//go:generate mockgen -package mocks -destination=../../../mocks/$GOFILE -source=$GOFILE -build_flags=-mod=vendor
//go:generate go tool mockgen -package mocks -destination=../../../mocks/$GOFILE -source=$GOFILE
import "net/http"
+4 -18
View File
@@ -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()
@@ -191,24 +193,8 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) {
}
// SetPostgresCRDStatus of Postgres cluster
func (client *KubernetesClient) SetPostgresCRDStatus(clusterName spec.NamespacedName, status string) (*apiacidv1.Postgresql, error) {
var pg *apiacidv1.Postgresql
var pgStatus apiacidv1.PostgresStatus
pgStatus.PostgresClusterStatus = status
patch, err := json.Marshal(struct {
PgStatus interface{} `json:"status"`
}{&pgStatus})
if err != nil {
return pg, fmt.Errorf("could not marshal status: %v", err)
}
// we cannot do a full scale update here without fetching the previous manifest (as the resourceVersion may differ),
// however, we could do patch without it. In the future, once /status subresource is there (starting Kubernetes 1.11)
// we should take advantage of it.
pg, err = client.PostgresqlsGetter.Postgresqls(clusterName.Namespace).Patch(
context.TODO(), clusterName.Name, types.MergePatchType, patch, metav1.PatchOptions{}, "status")
func (client *KubernetesClient) SetPostgresCRDStatus(clusterName spec.NamespacedName, pg *apiacidv1.Postgresql) (*apiacidv1.Postgresql, error) {
pg, err := client.PostgresqlsGetter.Postgresqls(clusterName.Namespace).UpdateStatus(context.TODO(), pg, metav1.UpdateOptions{})
if err != nil {
return pg, fmt.Errorf("could not update status: %v", err)
}
+4
View File
@@ -48,6 +48,10 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM
if newUser.Deleted {
continue
}
// when the secret of the user could not be created or updated skip any database actions
if newUser.Degraded {
continue
}
dbUser, exists := dbUsers[name]
if !exists {
reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncUserAdd, User: newUser})
+5 -4
View File
@@ -88,12 +88,13 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti
}
for _, v := range volumeOutput.Volumes {
if *v.VolumeType == "gp3" {
switch *v.VolumeType {
case "gp3":
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: *v.Size, VolumeType: *v.VolumeType, Iops: *v.Iops, Throughput: *v.Throughput})
} else if *v.VolumeType == "gp2" {
case "gp2":
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: *v.Size, VolumeType: *v.VolumeType})
} else {
return nil, fmt.Errorf("Discovered unexpected volume type %s %s", *v.VolumeId, *v.VolumeType)
default:
return nil, fmt.Errorf("discovered unexpected volume type %s %s", *v.VolumeId, *v.VolumeType)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
package volumes
//go:generate mockgen -package mocks -destination=../../../mocks/$GOFILE -source=$GOFILE -build_flags=-mod=vendor
//go:generate go tool mockgen -package mocks -destination=../../../mocks/$GOFILE -source=$GOFILE
import v1 "k8s.io/api/core/v1"