Add Patroni failsafe_mode parameter (#2076)

This commit adds support of a not-yet-released Patroni feature that allows postgres to run as primary in case of a failed leader lock update.
* Add Patroni 'failsafe_mode' local parameter (enable for a single PG cluster)
* Allow configuring Patroni 'failsafe_mode' parameter globally
This commit is contained in:
Polina Bungina 2022-12-02 13:33:02 +01:00 committed by GitHub
parent 1d44dd4694
commit 4d585250db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 297 additions and 50 deletions

View File

@ -633,6 +633,12 @@ spec:
type: string type: string
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
default: "100Mi" default: "100Mi"
patroni:
type: object
properties:
failsafe_mode:
type: boolean
default: false
status: status:
type: object type: object
additionalProperties: additionalProperties:

View File

@ -320,6 +320,8 @@ spec:
patroni: patroni:
type: object type: object
properties: properties:
failsafe_mode:
type: boolean
initdb: initdb:
type: object type: object
additionalProperties: additionalProperties:

View File

@ -409,6 +409,10 @@ configConnectionPooler:
connection_pooler_default_cpu_limit: "1" connection_pooler_default_cpu_limit: "1"
connection_pooler_default_memory_limit: 100Mi connection_pooler_default_memory_limit: 100Mi
configPatroni:
# enable Patroni DCS failsafe_mode feature
failsafe_mode: false
# Zalando's internal CDC stream feature # Zalando's internal CDC stream feature
enableStreams: false enableStreams: false

View File

@ -316,6 +316,9 @@ explanation of `ttl` and `loop_wait` parameters.
* **synchronous_node_count** * **synchronous_node_count**
Patroni `synchronous_node_count` parameter value. Note, this option is only available for Spilo images with Patroni 2.0+. The default is set to `1`. Optional. Patroni `synchronous_node_count` parameter value. Note, this option is only available for Spilo images with Patroni 2.0+. The default is set to `1`. Optional.
* **failsafe_mode**
Patroni `failsafe_mode` parameter value. If enabled, allows Patroni to cope with DCS outages and avoid leader demotion. Note, this option is currently not included in any Patroni release. The default is set to `false`. Optional.
## Postgres container resources ## Postgres container resources

View File

@ -407,7 +407,8 @@ class EndToEndTestCase(unittest.TestCase):
"ttl": 29, "ttl": 29,
"loop_wait": 9, "loop_wait": 9,
"retry_timeout": 9, "retry_timeout": 9,
"synchronous_mode": True "synchronous_mode": True,
"failsafe_mode": True,
} }
} }
} }
@ -434,6 +435,8 @@ class EndToEndTestCase(unittest.TestCase):
"retry_timeout not updated") "retry_timeout not updated")
self.assertEqual(desired_config["synchronous_mode"], effective_config["synchronous_mode"], self.assertEqual(desired_config["synchronous_mode"], effective_config["synchronous_mode"],
"synchronous_mode not updated") "synchronous_mode not updated")
self.assertEqual(desired_config["failsafe_mode"], effective_config["failsafe_mode"],
"failsafe_mode not updated")
return True return True
# check if Patroni config has been updated # check if Patroni config has been updated

View File

@ -109,6 +109,7 @@ spec:
cpu: 500m cpu: 500m
memory: 500Mi memory: 500Mi
patroni: patroni:
failsafe_mode: false
initdb: initdb:
encoding: "UTF8" encoding: "UTF8"
locale: "en_US.UTF-8" locale: "en_US.UTF-8"

View File

@ -47,6 +47,7 @@ data:
enable_master_load_balancer: "false" enable_master_load_balancer: "false"
enable_master_pooler_load_balancer: "false" enable_master_pooler_load_balancer: "false"
enable_password_rotation: "false" enable_password_rotation: "false"
# enable_patroni_failsafe_mode: "false"
enable_pgversion_env_var: "true" enable_pgversion_env_var: "true"
# enable_pod_antiaffinity: "false" # enable_pod_antiaffinity: "false"
# enable_pod_disruption_budget: "true" # enable_pod_disruption_budget: "true"

View File

@ -631,6 +631,12 @@ spec:
type: string type: string
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
default: "100Mi" default: "100Mi"
patroni:
type: object
properties:
failsafe_mode:
type: boolean
default: false
status: status:
type: object type: object
additionalProperties: additionalProperties:

View File

@ -198,3 +198,5 @@ configuration:
connection_pooler_number_of_instances: 2 connection_pooler_number_of_instances: 2
# connection_pooler_schema: "pooler" # connection_pooler_schema: "pooler"
# connection_pooler_user: "pooler" # connection_pooler_user: "pooler"
patroni:
# failsafe_mode: "false"

View File

@ -318,6 +318,8 @@ spec:
patroni: patroni:
type: object type: object
properties: properties:
failsafe_mode:
type: boolean
initdb: initdb:
type: object type: object
additionalProperties: additionalProperties:

View File

@ -503,6 +503,9 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
"patroni": { "patroni": {
Type: "object", Type: "object",
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"failsafe_mode": {
Type: "boolean",
},
"initdb": { "initdb": {
Type: "object", Type: "object",
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
@ -1458,6 +1461,14 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
}, },
}, },
}, },
"patroni": {
Type: "object",
Properties: map[string]apiextv1.JSONSchemaProps{
"failsafe_mode": {
Type: "boolean",
},
},
},
"postgres_pod_resources": { "postgres_pod_resources": {
Type: "object", Type: "object",
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{

View File

@ -227,6 +227,11 @@ type OperatorLogicalBackupConfiguration struct {
JobPrefix string `json:"logical_backup_job_prefix,omitempty"` JobPrefix string `json:"logical_backup_job_prefix,omitempty"`
} }
// PatroniConfiguration defines configuration for Patroni
type PatroniConfiguration struct {
FailsafeMode *bool `json:"failsafe_mode,omitempty"`
}
// OperatorConfigurationData defines the operation config // OperatorConfigurationData defines the operation config
type OperatorConfigurationData struct { type OperatorConfigurationData struct {
EnableCRDRegistration *bool `json:"enable_crd_registration,omitempty"` EnableCRDRegistration *bool `json:"enable_crd_registration,omitempty"`
@ -259,11 +264,12 @@ type OperatorConfigurationData struct {
Scalyr ScalyrConfiguration `json:"scalyr"` Scalyr ScalyrConfiguration `json:"scalyr"`
LogicalBackup OperatorLogicalBackupConfiguration `json:"logical_backup"` LogicalBackup OperatorLogicalBackupConfiguration `json:"logical_backup"`
ConnectionPooler ConnectionPoolerConfiguration `json:"connection_pooler"` ConnectionPooler ConnectionPoolerConfiguration `json:"connection_pooler"`
Patroni PatroniConfiguration `json:"patroni"`
MinInstances int32 `json:"min_instances,omitempty"` MinInstances int32 `json:"min_instances,omitempty"`
MaxInstances int32 `json:"max_instances,omitempty"` MaxInstances int32 `json:"max_instances,omitempty"`
IgnoreInstanceLimitsAnnotationKey string `json:"ignore_instance_limits_annotation_key,omitempty"` IgnoreInstanceLimitsAnnotationKey string `json:"ignore_instance_limits_annotation_key,omitempty"`
} }
//Duration shortens this frequently used name // Duration shortens this frequently used name
type Duration time.Duration type Duration time.Duration

View File

@ -171,6 +171,7 @@ type Patroni struct {
SynchronousMode bool `json:"synchronous_mode,omitempty"` SynchronousMode bool `json:"synchronous_mode,omitempty"`
SynchronousModeStrict bool `json:"synchronous_mode_strict,omitempty"` SynchronousModeStrict bool `json:"synchronous_mode_strict,omitempty"`
SynchronousNodeCount uint32 `json:"synchronous_node_count,omitempty" defaults:"1"` SynchronousNodeCount uint32 `json:"synchronous_node_count,omitempty" defaults:"1"`
FailsafeMode *bool `json:"failsafe_mode,omitempty"`
} }
// StandbyDescription contains remote primary config or s3/gs wal path // StandbyDescription contains remote primary config or s3/gs wal path

View File

@ -423,6 +423,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
out.Scalyr = in.Scalyr out.Scalyr = in.Scalyr
out.LogicalBackup = in.LogicalBackup out.LogicalBackup = in.LogicalBackup
in.ConnectionPooler.DeepCopyInto(&out.ConnectionPooler) in.ConnectionPooler.DeepCopyInto(&out.ConnectionPooler)
in.Patroni.DeepCopyInto(&out.Patroni)
return return
} }
@ -549,6 +550,11 @@ func (in *Patroni) DeepCopyInto(out *Patroni) {
(*out)[key] = outVal (*out)[key] = outVal
} }
} }
if in.FailsafeMode != nil {
in, out := &in.FailsafeMode, &out.FailsafeMode
*out = new(bool)
**out = **in
}
return return
} }
@ -562,6 +568,27 @@ func (in *Patroni) DeepCopy() *Patroni {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PatroniConfiguration) DeepCopyInto(out *PatroniConfiguration) {
*out = *in
if in.FailsafeMode != nil {
in, out := &in.FailsafeMode, &out.FailsafeMode
*out = new(bool)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatroniConfiguration.
func (in *PatroniConfiguration) DeepCopy() *PatroniConfiguration {
if in == nil {
return nil
}
out := new(PatroniConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgresPodResourcesDefaults) DeepCopyInto(out *PostgresPodResourcesDefaults) { func (in *PostgresPodResourcesDefaults) DeepCopyInto(out *PostgresPodResourcesDefaults) {
*out = *in *out = *in

View File

@ -59,6 +59,7 @@ type patroniDCS struct {
SynchronousNodeCount uint32 `json:"synchronous_node_count,omitempty"` SynchronousNodeCount uint32 `json:"synchronous_node_count,omitempty"`
PGBootstrapConfiguration map[string]interface{} `json:"postgresql,omitempty"` PGBootstrapConfiguration map[string]interface{} `json:"postgresql,omitempty"`
Slots map[string]map[string]string `json:"slots,omitempty"` Slots map[string]map[string]string `json:"slots,omitempty"`
FailsafeMode *bool `json:"failsafe_mode,omitempty"`
} }
type pgBootstrap struct { type pgBootstrap struct {
@ -296,7 +297,7 @@ func (c *Cluster) generateResourceRequirements(
return &result, nil return &result, nil
} }
func generateSpiloJSONConfiguration(pg *acidv1.PostgresqlParam, patroni *acidv1.Patroni, pamRoleName string, EnablePgVersionEnvVar bool, logger *logrus.Entry) (string, error) { func generateSpiloJSONConfiguration(pg *acidv1.PostgresqlParam, patroni *acidv1.Patroni, opConfig *config.Config, logger *logrus.Entry) (string, error) {
config := spiloConfiguration{} config := spiloConfiguration{}
config.Bootstrap = pgBootstrap{} config.Bootstrap = pgBootstrap{}
@ -378,6 +379,11 @@ PatroniInitDBParams:
if patroni.SynchronousNodeCount >= 1 { if patroni.SynchronousNodeCount >= 1 {
config.Bootstrap.DCS.SynchronousNodeCount = patroni.SynchronousNodeCount config.Bootstrap.DCS.SynchronousNodeCount = patroni.SynchronousNodeCount
} }
if patroni.FailsafeMode != nil {
config.Bootstrap.DCS.FailsafeMode = patroni.FailsafeMode
} else if opConfig.EnablePatroniFailsafeMode != nil {
config.Bootstrap.DCS.FailsafeMode = opConfig.EnablePatroniFailsafeMode
}
config.PgLocalConfiguration = make(map[string]interface{}) config.PgLocalConfiguration = make(map[string]interface{})
@ -385,7 +391,7 @@ PatroniInitDBParams:
// setting postgresq.bin_dir in the SPILO_CONFIGURATION still works and takes precedence over PGVERSION // setting postgresq.bin_dir in the SPILO_CONFIGURATION still works and takes precedence over PGVERSION
// so we add postgresq.bin_dir only if PGVERSION is unused // so we add postgresq.bin_dir only if PGVERSION is unused
// see PR 222 in Spilo // see PR 222 in Spilo
if !EnablePgVersionEnvVar { if !opConfig.EnablePgVersionEnvVar {
config.PgLocalConfiguration[patroniPGBinariesParameterName] = fmt.Sprintf(pgBinariesLocationTemplate, pg.PgVersion) config.PgLocalConfiguration[patroniPGBinariesParameterName] = fmt.Sprintf(pgBinariesLocationTemplate, pg.PgVersion)
} }
if len(pg.Parameters) > 0 { if len(pg.Parameters) > 0 {
@ -407,7 +413,7 @@ PatroniInitDBParams:
} }
config.Bootstrap.Users = map[string]pgUser{ config.Bootstrap.Users = map[string]pgUser{
pamRoleName: { opConfig.PamRoleName: {
Password: "", Password: "",
Options: []string{constants.RoleFlagCreateDB, constants.RoleFlagNoLogin}, Options: []string{constants.RoleFlagCreateDB, constants.RoleFlagNoLogin},
}, },
@ -1179,7 +1185,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
} }
} }
spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, c.OpConfig.PamRoleName, c.OpConfig.EnablePgVersionEnvVar, c.logger) spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, &c.OpConfig, c.logger)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not generate Spilo JSON configuration: %v", err) return nil, fmt.Errorf("could not generate Spilo JSON configuration: %v", err)
} }

View File

@ -69,17 +69,19 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) {
subtest string subtest string
pgParam *acidv1.PostgresqlParam pgParam *acidv1.PostgresqlParam
patroni *acidv1.Patroni patroni *acidv1.Patroni
role string opConfig *config.Config
opConfig config.Config
result string result string
}{ }{
{ {
subtest: "Patroni default configuration", subtest: "Patroni default configuration",
pgParam: &acidv1.PostgresqlParam{PgVersion: "9.6"}, pgParam: &acidv1.PostgresqlParam{PgVersion: "9.6"},
patroni: &acidv1.Patroni{}, patroni: &acidv1.Patroni{},
role: "zalandos", opConfig: &config.Config{
opConfig: config.Config{}, Auth: config.Auth{
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/9.6/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{}}}`, PamRoleName: "zalandos",
},
},
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/9.6/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{}}}`,
}, },
{ {
subtest: "Patroni configured", subtest: "Patroni configured",
@ -99,21 +101,65 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) {
SynchronousModeStrict: true, SynchronousModeStrict: true,
SynchronousNodeCount: 1, SynchronousNodeCount: 1,
Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}}, Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}},
FailsafeMode: util.True(),
}, },
role: "zalandos", opConfig: &config.Config{
opConfig: config.Config{}, Auth: config.Auth{
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/11/bin","pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}}}}`, PamRoleName: "zalandos",
},
},
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/11/bin","pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"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",
pgParam: &acidv1.PostgresqlParam{PgVersion: "14"},
patroni: &acidv1.Patroni{},
opConfig: &config.Config{
Auth: config.Auth{
PamRoleName: "zalandos",
},
EnablePatroniFailsafeMode: util.True(),
},
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/14/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":true}}}`,
},
{
subtest: "Patroni failsafe_mode configured globally, disabled for cluster",
pgParam: &acidv1.PostgresqlParam{PgVersion: "14"},
patroni: &acidv1.Patroni{
FailsafeMode: util.False(),
},
opConfig: &config.Config{
Auth: config.Auth{
PamRoleName: "zalandos",
},
EnablePatroniFailsafeMode: util.True(),
},
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/14/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":false}}}`,
},
{
subtest: "Patroni failsafe_mode disabled globally, configured for cluster",
pgParam: &acidv1.PostgresqlParam{PgVersion: "14"},
patroni: &acidv1.Patroni{
FailsafeMode: util.True(),
},
opConfig: &config.Config{
Auth: config.Auth{
PamRoleName: "zalandos",
},
EnablePatroniFailsafeMode: util.False(),
},
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/14/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":true}}}`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
cluster.OpConfig = tt.opConfig cluster.OpConfig = *tt.opConfig
result, err := generateSpiloJSONConfiguration(tt.pgParam, tt.patroni, tt.role, false, logger) result, err := generateSpiloJSONConfiguration(tt.pgParam, tt.patroni, tt.opConfig, logger)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
if tt.result != result { if tt.result != result {
t.Errorf("%s %s: Spilo Config is %v, expected %v for role %#v and param %#v", t.Errorf("%s %s: Spilo Config is %v, expected %v for role %#v and param %#v",
testName, tt.subtest, result, tt.result, tt.role, tt.pgParam) testName, tt.subtest, result, tt.result, tt.opConfig.Auth.PamRoleName, tt.pgParam)
} }
} }
} }

View File

@ -564,6 +564,21 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv
configToSet["ttl"] = desiredPatroniConfig.TTL configToSet["ttl"] = desiredPatroniConfig.TTL
} }
var desiredFailsafe *bool
if desiredPatroniConfig.FailsafeMode != nil {
desiredFailsafe = desiredPatroniConfig.FailsafeMode
} else if c.OpConfig.EnablePatroniFailsafeMode != nil {
desiredFailsafe = c.OpConfig.EnablePatroniFailsafeMode
}
effectiveFailsafe := effectivePatroniConfig.FailsafeMode
if desiredFailsafe != nil {
if effectiveFailsafe == nil || *desiredFailsafe != *effectiveFailsafe {
configToSet["failsafe_mode"] = *desiredFailsafe
}
}
// check if specified slots exist in config and if they differ // check if specified slots exist in config and if they differ
slotsToSet := make(map[string]map[string]string) slotsToSet := make(map[string]map[string]string)
for slotName, desiredSlot := range desiredPatroniConfig.Slots { for slotName, desiredSlot := range desiredPatroniConfig.Slots {

View File

@ -20,6 +20,7 @@ import (
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
fakeacidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake" fakeacidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake"
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/k8sutil"
"github.com/zalando/postgres-operator/pkg/util/patroni" "github.com/zalando/postgres-operator/pkg/util/patroni"
@ -147,20 +148,23 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
defer ctrl.Finish() defer ctrl.Finish()
defaultPgParameters := map[string]string{
"log_min_duration_statement": "200",
"max_connections": "50",
}
defaultPatroniParameters := acidv1.Patroni{
TTL: 20,
}
pg := acidv1.Postgresql{ pg := acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: clusterName, Name: clusterName,
Namespace: namespace, Namespace: namespace,
}, },
Spec: acidv1.PostgresSpec{ Spec: acidv1.PostgresSpec{
Patroni: acidv1.Patroni{ Patroni: defaultPatroniParameters,
TTL: 20,
},
PostgresqlParam: acidv1.PostgresqlParam{ PostgresqlParam: acidv1.PostgresqlParam{
Parameters: map[string]string{ Parameters: defaultPgParameters,
"log_min_duration_statement": "200",
"max_connections": "50",
},
}, },
Volume: acidv1.Volume{ Volume: acidv1.Volume{
Size: "1Gi", Size: "1Gi",
@ -222,9 +226,7 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) {
}, },
{ {
subtest: "multiple Postgresql.Parameters differ - restart replica first", subtest: "multiple Postgresql.Parameters differ - restart replica first",
patroni: acidv1.Patroni{ patroni: defaultPatroniParameters,
TTL: 20,
},
pgParams: map[string]string{ pgParams: map[string]string{
"log_min_duration_statement": "500", // desired 200 "log_min_duration_statement": "500", // desired 200
"max_connections": "100", // desired 50 "max_connections": "100", // desired 50
@ -233,9 +235,7 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) {
}, },
{ {
subtest: "desired max_connections bigger - restart replica first", subtest: "desired max_connections bigger - restart replica first",
patroni: acidv1.Patroni{ patroni: defaultPatroniParameters,
TTL: 20,
},
pgParams: map[string]string{ pgParams: map[string]string{
"log_min_duration_statement": "200", "log_min_duration_statement": "200",
"max_connections": "30", // desired 50 "max_connections": "30", // desired 50
@ -244,9 +244,7 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) {
}, },
{ {
subtest: "desired max_connections smaller - restart master first", subtest: "desired max_connections smaller - restart master first",
patroni: acidv1.Patroni{ patroni: defaultPatroniParameters,
TTL: 20,
},
pgParams: map[string]string{ pgParams: map[string]string{
"log_min_duration_statement": "200", "log_min_duration_statement": "200",
"max_connections": "100", // desired 50 "max_connections": "100", // desired 50
@ -265,6 +263,109 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) {
t.Errorf("%s - %s: wrong master restart strategy, got restart %v, expected restart %v", testName, tt.subtest, requirePrimaryRestart, tt.restartPrimary) t.Errorf("%s - %s: wrong master restart strategy, got restart %v, expected restart %v", testName, tt.subtest, requirePrimaryRestart, tt.restartPrimary)
} }
} }
testsFailsafe := []struct {
subtest string
operatorVal *bool
effectiveVal *bool
desiredVal bool
shouldBePatched bool
restartPrimary bool
}{
{
subtest: "Not set in operator config, not set for pg cluster. Set to true in the pg config.",
operatorVal: nil,
effectiveVal: nil,
desiredVal: true,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Not set in operator config, disabled for pg cluster. Set to true in the pg config.",
operatorVal: nil,
effectiveVal: util.False(),
desiredVal: true,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Not set in operator config, not set for pg cluster. Set to false in the pg config.",
operatorVal: nil,
effectiveVal: nil,
desiredVal: false,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Not set in operator config, enabled for pg cluster. Set to false in the pg config.",
operatorVal: nil,
effectiveVal: util.True(),
desiredVal: false,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Enabled in operator config, not set for pg cluster. Set to false in the pg config.",
operatorVal: util.True(),
effectiveVal: nil,
desiredVal: false,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Enabled in operator config, disabled for pg cluster. Set to true in the pg config.",
operatorVal: util.True(),
effectiveVal: util.False(),
desiredVal: true,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Disabled in operator config, not set for pg cluster. Set to true in the pg config.",
operatorVal: util.False(),
effectiveVal: nil,
desiredVal: true,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Disabled in operator config, enabled for pg cluster. Set to false in the pg config.",
operatorVal: util.False(),
effectiveVal: util.True(),
desiredVal: false,
shouldBePatched: true,
restartPrimary: false,
},
{
subtest: "Disabled in operator config, enabled for pg cluster. Set to true in the pg config.",
operatorVal: util.False(),
effectiveVal: util.True(),
desiredVal: true,
shouldBePatched: false, // should not require patching
restartPrimary: true,
},
}
for _, tt := range testsFailsafe {
patroniConf := defaultPatroniParameters
if tt.operatorVal != nil {
cluster.OpConfig.EnablePatroniFailsafeMode = tt.operatorVal
}
if tt.effectiveVal != nil {
patroniConf.FailsafeMode = tt.effectiveVal
}
cluster.Spec.Patroni.FailsafeMode = &tt.desiredVal
configPatched, requirePrimaryRestart, err := cluster.checkAndSetGlobalPostgreSQLConfiguration(mockPod, patroniConf, cluster.Spec.Patroni, defaultPgParameters, cluster.Spec.Parameters)
assert.NoError(t, err)
if configPatched != tt.shouldBePatched {
t.Errorf("%s - %s: expected update went wrong", testName, tt.subtest)
}
if requirePrimaryRestart != tt.restartPrimary {
t.Errorf("%s - %s: wrong master restart strategy, got restart %v, expected restart %v", testName, tt.subtest, requirePrimaryRestart, tt.restartPrimary)
}
}
} }
func TestUpdateSecret(t *testing.T) { func TestUpdateSecret(t *testing.T) {

View File

@ -216,6 +216,9 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.ScalyrCPULimit = fromCRD.Scalyr.ScalyrCPULimit result.ScalyrCPULimit = fromCRD.Scalyr.ScalyrCPULimit
result.ScalyrMemoryLimit = fromCRD.Scalyr.ScalyrMemoryLimit result.ScalyrMemoryLimit = fromCRD.Scalyr.ScalyrMemoryLimit
// Patroni config
result.EnablePatroniFailsafeMode = util.CoalesceBool(fromCRD.Patroni.FailsafeMode, util.False())
// Connection pooler. Looks like we can't use defaulting in CRD before 1.17, // Connection pooler. Looks like we can't use defaulting in CRD before 1.17,
// so ensure default values here. // so ensure default values here.
result.ConnectionPooler.NumberOfInstances = util.CoalesceInt32( result.ConnectionPooler.NumberOfInstances = util.CoalesceInt32(

View File

@ -45,14 +45,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
// AddToScheme adds all types of this clientset into the given scheme. This allows composition // AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in: // of clientsets, like in:
// //
// import ( // import (
// "k8s.io/client-go/kubernetes" // "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme" // clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// ) // )
// //
// kclientset, _ := kubernetes.NewForConfig(c) // kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
// //
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly. // correctly.

View File

@ -45,14 +45,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
// AddToScheme adds all types of this clientset into the given scheme. This allows composition // AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in: // of clientsets, like in:
// //
// import ( // import (
// "k8s.io/client-go/kubernetes" // "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme" // clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// ) // )
// //
// kclientset, _ := kubernetes.NewForConfig(c) // kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
// //
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly. // correctly.

View File

@ -234,6 +234,7 @@ type Config struct {
TargetMajorVersion string `name:"target_major_version" default:"14"` TargetMajorVersion string `name:"target_major_version" default:"14"`
PatroniAPICheckInterval time.Duration `name:"patroni_api_check_interval" default:"1s"` PatroniAPICheckInterval time.Duration `name:"patroni_api_check_interval" default:"1s"`
PatroniAPICheckTimeout time.Duration `name:"patroni_api_check_timeout" default:"5s"` PatroniAPICheckTimeout time.Duration `name:"patroni_api_check_timeout" default:"5s"`
EnablePatroniFailsafeMode *bool `name:"enable_patroni_failsafe_mode" default:"false"`
} }
// MustMarshal marshals the config or panics // MustMarshal marshals the config or panics