restart instances via rest api instead of recreating pods, fixes bug with being unable to decrease some values, like max_connections (#1103)
* restart instances via rest api instead of recreating pods * Ignore differences in bootstrap.dcs when compare SPILO_CONFIGURATION * isBootstrapOnlyParameter is rewritten, instead of whitelist it uses blacklist * added e2e test for max_connections decreasing * documentation updated * pending_restart flag added to restart api call, wait fot ttl seconds after restart * refactoring, /restart returns error if pending_restart is set to true and patroni is not pending restart * restart postgresql instances within pods only if pod's restart is not required * patroni might need to restart postgresql after pods were recreated if values like max_connections decreased * instancesRestart is not critical, try to restart pods if not successful * cleanup Co-authored-by: Felix Kunde <felix-kunde@gmx.de>
This commit is contained in:
parent
75a9e2be38
commit
ebb3204cdd
|
|
@ -168,6 +168,10 @@ operator checks during Sync all pods run images specified in their respective
|
||||||
statefulsets. The operator triggers a rolling upgrade for PG clusters that
|
statefulsets. The operator triggers a rolling upgrade for PG clusters that
|
||||||
violate this condition.
|
violate this condition.
|
||||||
|
|
||||||
|
Changes in $SPILO\_CONFIGURATION under path bootstrap.dcs are ignored when
|
||||||
|
StatefulSets are being compared, if there are changes under this path, they are
|
||||||
|
applied through rest api interface and following restart of patroni instance
|
||||||
|
|
||||||
## Delete protection via annotations
|
## Delete protection via annotations
|
||||||
|
|
||||||
To avoid accidental deletes of Postgres clusters the operator can check the
|
To avoid accidental deletes of Postgres clusters the operator can check the
|
||||||
|
|
|
||||||
|
|
@ -1418,6 +1418,54 @@ class EndToEndTestCase(unittest.TestCase):
|
||||||
}
|
}
|
||||||
k8s.update_config(patch_delete_annotations)
|
k8s.update_config(patch_delete_annotations)
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_decrease_max_connections(self):
|
||||||
|
'''
|
||||||
|
Test decreasing max_connections and restarting cluster through rest api
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||||
|
labels = 'spilo-role=master,' + cluster_label
|
||||||
|
new_max_connections_value = "99"
|
||||||
|
pods = k8s.api.core_v1.list_namespaced_pod(
|
||||||
|
'default', label_selector=labels).items
|
||||||
|
self.assert_master_is_unique()
|
||||||
|
masterPod = pods[0]
|
||||||
|
creationTimestamp = masterPod.metadata.creation_timestamp
|
||||||
|
|
||||||
|
# adjust max_connection
|
||||||
|
pg_patch_max_connections = {
|
||||||
|
"spec": {
|
||||||
|
"postgresql": {
|
||||||
|
"parameters": {
|
||||||
|
"max_connections": new_max_connections_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_max_connections)
|
||||||
|
|
||||||
|
def get_max_connections():
|
||||||
|
pods = k8s.api.core_v1.list_namespaced_pod(
|
||||||
|
'default', label_selector=labels).items
|
||||||
|
self.assert_master_is_unique()
|
||||||
|
masterPod = pods[0]
|
||||||
|
get_max_connections_cmd = '''psql -At -U postgres -c "SELECT setting FROM pg_settings WHERE name = 'max_connections';"'''
|
||||||
|
result = k8s.exec_with_kubectl(masterPod.metadata.name, get_max_connections_cmd)
|
||||||
|
max_connections_value = int(result.stdout)
|
||||||
|
return max_connections_value
|
||||||
|
|
||||||
|
#Make sure that max_connections decreased
|
||||||
|
self.eventuallyEqual(get_max_connections, int(new_max_connections_value), "max_connections didn't decrease")
|
||||||
|
pods = k8s.api.core_v1.list_namespaced_pod(
|
||||||
|
'default', label_selector=labels).items
|
||||||
|
self.assert_master_is_unique()
|
||||||
|
masterPod = pods[0]
|
||||||
|
#Make sure that pod didn't restart
|
||||||
|
self.assertEqual(creationTimestamp, masterPod.metadata.creation_timestamp,
|
||||||
|
"Master pod creation timestamp is updated")
|
||||||
|
|
||||||
def get_failover_targets(self, master_node, replica_nodes):
|
def get_failover_targets(self, master_node, replica_nodes):
|
||||||
'''
|
'''
|
||||||
If all pods live on the same node, failover will happen to other worker(s)
|
If all pods live on the same node, failover will happen to other worker(s)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package cluster
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -519,7 +520,7 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe
|
||||||
newCheck("new statefulset %s's %s (index %d) resources do not match the current ones",
|
newCheck("new statefulset %s's %s (index %d) resources do not match the current ones",
|
||||||
func(a, b v1.Container) bool { return !compareResources(&a.Resources, &b.Resources) }),
|
func(a, b v1.Container) bool { return !compareResources(&a.Resources, &b.Resources) }),
|
||||||
newCheck("new statefulset %s's %s (index %d) environment does not match the current one",
|
newCheck("new statefulset %s's %s (index %d) environment does not match the current one",
|
||||||
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.Env, b.Env) }),
|
func(a, b v1.Container) bool { return !compareEnv(a.Env, b.Env) }),
|
||||||
newCheck("new statefulset %s's %s (index %d) environment sources do not match the current one",
|
newCheck("new statefulset %s's %s (index %d) environment sources do not match the current one",
|
||||||
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.EnvFrom, b.EnvFrom) }),
|
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.EnvFrom, b.EnvFrom) }),
|
||||||
newCheck("new statefulset %s's %s (index %d) security context does not match the current one",
|
newCheck("new statefulset %s's %s (index %d) security context does not match the current one",
|
||||||
|
|
@ -576,6 +577,56 @@ func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.Resourc
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compareEnv(a, b []v1.EnvVar) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
equal := true
|
||||||
|
for _, enva := range a {
|
||||||
|
hasmatch := false
|
||||||
|
for _, envb := range b {
|
||||||
|
if enva.Name == envb.Name {
|
||||||
|
hasmatch = true
|
||||||
|
if enva.Name == "SPILO_CONFIGURATION" {
|
||||||
|
equal = compareSpiloConfiguration(enva.Value, envb.Value)
|
||||||
|
} else {
|
||||||
|
if enva.Value == "" && envb.Value == "" {
|
||||||
|
equal = reflect.DeepEqual(enva.ValueFrom, envb.ValueFrom)
|
||||||
|
} else {
|
||||||
|
equal = (enva.Value == envb.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !equal {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasmatch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareSpiloConfiguration(configa, configb string) bool {
|
||||||
|
var (
|
||||||
|
oa, ob spiloConfiguration
|
||||||
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
err = json.Unmarshal([]byte(configa), &oa)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
oa.Bootstrap.DCS = patroniDCS{}
|
||||||
|
err = json.Unmarshal([]byte(configb), &ob)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ob.Bootstrap.DCS = patroniDCS{}
|
||||||
|
return reflect.DeepEqual(oa, ob)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cluster) enforceMinResourceLimits(spec *acidv1.PostgresSpec) error {
|
func (c *Cluster) enforceMinResourceLimits(spec *acidv1.PostgresSpec) error {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/zalando/postgres-operator/pkg/util/constants"
|
"github.com/zalando/postgres-operator/pkg/util/constants"
|
||||||
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
|
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
|
||||||
"github.com/zalando/postgres-operator/pkg/util/teams"
|
"github.com/zalando/postgres-operator/pkg/util/teams"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
"k8s.io/client-go/tools/record"
|
"k8s.io/client-go/tools/record"
|
||||||
|
|
@ -848,6 +849,159 @@ func TestPreparedDatabases(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompareSpiloConfiguration(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Config string
|
||||||
|
ExpectedResult bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"200","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"200","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`{}`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`invalidjson`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
refCase := testCases[0]
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
if result := compareSpiloConfiguration(refCase.Config, testCase.Config); result != testCase.ExpectedResult {
|
||||||
|
t.Errorf("expected %v got %v", testCase.ExpectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareEnv(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Envs []v1.EnvVar
|
||||||
|
ExpectedResult bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Envs: []v1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "VARIABLE1",
|
||||||
|
Value: "value1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE2",
|
||||||
|
Value: "value2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE3",
|
||||||
|
Value: "value3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "SPILO_CONFIGURATION",
|
||||||
|
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Envs: []v1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "VARIABLE1",
|
||||||
|
Value: "value1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE2",
|
||||||
|
Value: "value2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE3",
|
||||||
|
Value: "value3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "SPILO_CONFIGURATION",
|
||||||
|
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Envs: []v1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "VARIABLE4",
|
||||||
|
Value: "value4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE2",
|
||||||
|
Value: "value2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE3",
|
||||||
|
Value: "value3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "SPILO_CONFIGURATION",
|
||||||
|
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectedResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Envs: []v1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "VARIABLE1",
|
||||||
|
Value: "value1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE2",
|
||||||
|
Value: "value2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE3",
|
||||||
|
Value: "value3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE4",
|
||||||
|
Value: "value4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "SPILO_CONFIGURATION",
|
||||||
|
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectedResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Envs: []v1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "VARIABLE1",
|
||||||
|
Value: "value1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VARIABLE2",
|
||||||
|
Value: "value2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "SPILO_CONFIGURATION",
|
||||||
|
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectedResult: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
refCase := testCases[0]
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
if result := compareEnv(refCase.Envs, testCase.Envs); result != testCase.ExpectedResult {
|
||||||
|
t.Errorf("expected %v got %v", testCase.ExpectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCrossNamespacedSecrets(t *testing.T) {
|
func TestCrossNamespacedSecrets(t *testing.T) {
|
||||||
testName := "test secrets in different namespace"
|
testName := "test secrets in different namespace"
|
||||||
clientSet := fake.NewSimpleClientset()
|
clientSet := fake.NewSimpleClientset()
|
||||||
|
|
|
||||||
|
|
@ -412,13 +412,33 @@ func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]stri
|
||||||
// Those parameters must go to the bootstrap/dcs/postgresql/parameters section.
|
// Those parameters must go to the bootstrap/dcs/postgresql/parameters section.
|
||||||
// See http://patroni.readthedocs.io/en/latest/dynamic_configuration.html.
|
// See http://patroni.readthedocs.io/en/latest/dynamic_configuration.html.
|
||||||
func isBootstrapOnlyParameter(param string) bool {
|
func isBootstrapOnlyParameter(param string) bool {
|
||||||
return param == "max_connections" ||
|
params := map[string]bool{
|
||||||
param == "max_locks_per_transaction" ||
|
"archive_command": false,
|
||||||
param == "max_worker_processes" ||
|
"shared_buffers": false,
|
||||||
param == "max_prepared_transactions" ||
|
"logging_collector": false,
|
||||||
param == "wal_level" ||
|
"log_destination": false,
|
||||||
param == "wal_log_hints" ||
|
"log_directory": false,
|
||||||
param == "track_commit_timestamp"
|
"log_filename": false,
|
||||||
|
"log_file_mode": false,
|
||||||
|
"log_rotation_age": false,
|
||||||
|
"log_truncate_on_rotation": false,
|
||||||
|
"ssl": false,
|
||||||
|
"ssl_ca_file": false,
|
||||||
|
"ssl_crl_file": false,
|
||||||
|
"ssl_cert_file": false,
|
||||||
|
"ssl_key_file": false,
|
||||||
|
"shared_preload_libraries": false,
|
||||||
|
"bg_mon.listen_address": false,
|
||||||
|
"bg_mon.history_buckets": false,
|
||||||
|
"pg_stat_statements.track_utility": false,
|
||||||
|
"extwlist.extensions": false,
|
||||||
|
"extwlist.custom_path": false,
|
||||||
|
}
|
||||||
|
result, ok := params[param]
|
||||||
|
if !ok {
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateVolumeMounts(volume acidv1.Volume) []v1.VolumeMount {
|
func generateVolumeMounts(volume acidv1.Volume) []v1.VolumeMount {
|
||||||
|
|
|
||||||
|
|
@ -1207,6 +1207,12 @@ func TestSidecars(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
spec = acidv1.PostgresSpec{
|
spec = acidv1.PostgresSpec{
|
||||||
|
PostgresqlParam: acidv1.PostgresqlParam{
|
||||||
|
PgVersion: "12.1",
|
||||||
|
Parameters: map[string]string{
|
||||||
|
"max_connections": "100",
|
||||||
|
},
|
||||||
|
},
|
||||||
TeamID: "myapp", NumberOfInstances: 1,
|
TeamID: "myapp", NumberOfInstances: 1,
|
||||||
Resources: acidv1.Resources{
|
Resources: acidv1.Resources{
|
||||||
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
|
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||||
"github.com/zalando/postgres-operator/pkg/spec"
|
"github.com/zalando/postgres-operator/pkg/spec"
|
||||||
|
|
@ -260,6 +261,7 @@ func (c *Cluster) syncPodDisruptionBudget(isUpdate bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) syncStatefulSet() error {
|
func (c *Cluster) syncStatefulSet() error {
|
||||||
|
var instancesRestartRequired bool
|
||||||
|
|
||||||
podsToRecreate := make([]v1.Pod, 0)
|
podsToRecreate := make([]v1.Pod, 0)
|
||||||
switchoverCandidates := make([]spec.NamespacedName, 0)
|
switchoverCandidates := make([]spec.NamespacedName, 0)
|
||||||
|
|
@ -379,10 +381,21 @@ func (c *Cluster) syncStatefulSet() error {
|
||||||
// Apply special PostgreSQL parameters that can only be set via the Patroni API.
|
// Apply special PostgreSQL parameters that can only be set via the Patroni API.
|
||||||
// it is important to do it after the statefulset pods are there, but before the rolling update
|
// it is important to do it after the statefulset pods are there, but before the rolling update
|
||||||
// since those parameters require PostgreSQL restart.
|
// since those parameters require PostgreSQL restart.
|
||||||
if err := c.checkAndSetGlobalPostgreSQLConfiguration(); err != nil {
|
instancesRestartRequired, err = c.checkAndSetGlobalPostgreSQLConfiguration()
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("could not set cluster-wide PostgreSQL configuration options: %v", err)
|
return fmt.Errorf("could not set cluster-wide PostgreSQL configuration options: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if instancesRestartRequired {
|
||||||
|
c.logger.Debugln("restarting Postgres server within pods")
|
||||||
|
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Update", "restarting Postgres server within pods")
|
||||||
|
if err := c.restartInstances(); err != nil {
|
||||||
|
c.logger.Warningf("could not restart Postgres server within pods: %v", err)
|
||||||
|
}
|
||||||
|
c.logger.Infof("Postgres server successfuly restarted on all pods")
|
||||||
|
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Update", "Postgres server restart done - all instances have been restarted")
|
||||||
|
}
|
||||||
// if we get here we also need to re-create the pods (either leftovers from the old
|
// if we get here we also need to re-create the pods (either leftovers from the old
|
||||||
// statefulset or those that got their configuration from the outdated statefulset)
|
// statefulset or those that got their configuration from the outdated statefulset)
|
||||||
if len(podsToRecreate) > 0 {
|
if len(podsToRecreate) > 0 {
|
||||||
|
|
@ -396,6 +409,57 @@ func (c *Cluster) syncStatefulSet() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) restartInstances() error {
|
||||||
|
c.setProcessName("starting to restart Postgres servers")
|
||||||
|
ls := c.labelsSet(false)
|
||||||
|
namespace := c.Namespace
|
||||||
|
|
||||||
|
listOptions := metav1.ListOptions{
|
||||||
|
LabelSelector: ls.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := c.KubeClient.Pods(namespace).List(context.TODO(), listOptions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get the list of pods: %v", err)
|
||||||
|
}
|
||||||
|
c.logger.Infof("there are %d pods in the cluster which resquire Postgres server restart", len(pods.Items))
|
||||||
|
|
||||||
|
var (
|
||||||
|
masterPod *v1.Pod
|
||||||
|
)
|
||||||
|
for i, pod := range pods.Items {
|
||||||
|
role := PostgresRole(pod.Labels[c.OpConfig.PodRoleLabel])
|
||||||
|
|
||||||
|
if role == Master {
|
||||||
|
masterPod = &pods.Items[i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
podName := util.NameFromMeta(pods.Items[i].ObjectMeta)
|
||||||
|
config, err := c.patroni.GetConfig(&pod)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get config for pod %s: %v", podName, err)
|
||||||
|
}
|
||||||
|
ttl, ok := config["ttl"].(int32)
|
||||||
|
if !ok {
|
||||||
|
ttl = 30
|
||||||
|
}
|
||||||
|
if err = c.patroni.Restart(&pod); err != nil {
|
||||||
|
return fmt.Errorf("could not restart Postgres server on pod %s: %v", podName, err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(ttl) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if masterPod != nil {
|
||||||
|
podName := util.NameFromMeta(masterPod.ObjectMeta)
|
||||||
|
if err = c.patroni.Restart(masterPod); err != nil {
|
||||||
|
return fmt.Errorf("could not restart postgres server on masterPod %s: %v", podName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AnnotationsToPropagate get the annotations to update if required
|
// AnnotationsToPropagate get the annotations to update if required
|
||||||
// based on the annotations in postgres CRD
|
// based on the annotations in postgres CRD
|
||||||
func (c *Cluster) AnnotationsToPropagate(annotations map[string]string) map[string]string {
|
func (c *Cluster) AnnotationsToPropagate(annotations map[string]string) map[string]string {
|
||||||
|
|
@ -430,10 +494,11 @@ func (c *Cluster) AnnotationsToPropagate(annotations map[string]string) map[stri
|
||||||
|
|
||||||
// checkAndSetGlobalPostgreSQLConfiguration checks whether cluster-wide API parameters
|
// checkAndSetGlobalPostgreSQLConfiguration checks whether cluster-wide API parameters
|
||||||
// (like max_connections) has changed and if necessary sets it via the Patroni API
|
// (like max_connections) has changed and if necessary sets it via the Patroni API
|
||||||
func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration() error {
|
func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration() (bool, error) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
pods []v1.Pod
|
pods []v1.Pod
|
||||||
|
restartRequired bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// we need to extract those options from the cluster manifest.
|
// we need to extract those options from the cluster manifest.
|
||||||
|
|
@ -447,14 +512,14 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(optionsToSet) == 0 {
|
if len(optionsToSet) == 0 {
|
||||||
return nil
|
return restartRequired, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if pods, err = c.listPods(); err != nil {
|
if pods, err = c.listPods(); err != nil {
|
||||||
return err
|
return restartRequired, err
|
||||||
}
|
}
|
||||||
if len(pods) == 0 {
|
if len(pods) == 0 {
|
||||||
return fmt.Errorf("could not call Patroni API: cluster has no pods")
|
return restartRequired, fmt.Errorf("could not call Patroni API: cluster has no pods")
|
||||||
}
|
}
|
||||||
// try all pods until the first one that is successful, as it doesn't matter which pod
|
// try all pods until the first one that is successful, as it doesn't matter which pod
|
||||||
// carries the request to change configuration through
|
// carries the request to change configuration through
|
||||||
|
|
@ -463,11 +528,12 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration() error {
|
||||||
c.logger.Debugf("calling Patroni API on a pod %s to set the following Postgres options: %v",
|
c.logger.Debugf("calling Patroni API on a pod %s to set the following Postgres options: %v",
|
||||||
podName, optionsToSet)
|
podName, optionsToSet)
|
||||||
if err = c.patroni.SetPostgresParameters(&pod, optionsToSet); err == nil {
|
if err = c.patroni.SetPostgresParameters(&pod, optionsToSet); err == nil {
|
||||||
return nil
|
restartRequired = true
|
||||||
|
return restartRequired, nil
|
||||||
}
|
}
|
||||||
c.logger.Warningf("could not patch postgres parameters with a pod %s: %v", podName, err)
|
c.logger.Warningf("could not patch postgres parameters with a pod %s: %v", podName, err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("could not reach Patroni API to set Postgres options: failed on every pod (%d total)",
|
return restartRequired, fmt.Errorf("could not reach Patroni API to set Postgres options: failed on every pod (%d total)",
|
||||||
len(pods))
|
len(pods))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import (
|
||||||
const (
|
const (
|
||||||
failoverPath = "/failover"
|
failoverPath = "/failover"
|
||||||
configPath = "/config"
|
configPath = "/config"
|
||||||
|
statusPath = "/patroni"
|
||||||
|
restartPath = "/restart"
|
||||||
apiPort = 8008
|
apiPort = 8008
|
||||||
timeout = 30 * time.Second
|
timeout = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
@ -28,6 +30,8 @@ type Interface interface {
|
||||||
Switchover(master *v1.Pod, candidate string) error
|
Switchover(master *v1.Pod, candidate string) error
|
||||||
SetPostgresParameters(server *v1.Pod, options map[string]string) error
|
SetPostgresParameters(server *v1.Pod, options map[string]string) error
|
||||||
GetMemberData(server *v1.Pod) (MemberData, error)
|
GetMemberData(server *v1.Pod) (MemberData, error)
|
||||||
|
Restart(server *v1.Pod) error
|
||||||
|
GetConfig(server *v1.Pod) (map[string]interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patroni API client
|
// Patroni API client
|
||||||
|
|
@ -103,6 +107,32 @@ func (p *Patroni) httpPostOrPatch(method string, url string, body *bytes.Buffer)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Patroni) httpGet(url string) (string, error) {
|
||||||
|
request, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.logger.Debugf("making GET http request: %s", request.URL.String())
|
||||||
|
|
||||||
|
resp, err := p.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not make request: %v", err)
|
||||||
|
}
|
||||||
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not read response: %v", err)
|
||||||
|
}
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return "", fmt.Errorf("could not close request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return string(bodyBytes), fmt.Errorf("patroni returned '%d'", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return string(bodyBytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Switchover by calling Patroni REST API
|
// Switchover by calling Patroni REST API
|
||||||
func (p *Patroni) Switchover(master *v1.Pod, candidate string) error {
|
func (p *Patroni) Switchover(master *v1.Pod, candidate string) error {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
|
@ -149,6 +179,48 @@ type MemberData struct {
|
||||||
Patroni MemberDataPatroni `json:"patroni"`
|
Patroni MemberDataPatroni `json:"patroni"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Patroni) GetConfigOrStatus(server *v1.Pod, path string) (map[string]interface{}, error) {
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
apiURLString, err := apiURL(server)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
body, err := p.httpGet(apiURLString + path)
|
||||||
|
err = json.Unmarshal([]byte(body), &result)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Patroni) GetStatus(server *v1.Pod) (map[string]interface{}, error) {
|
||||||
|
return p.GetConfigOrStatus(server, statusPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Patroni) GetConfig(server *v1.Pod) (map[string]interface{}, error) {
|
||||||
|
return p.GetConfigOrStatus(server, configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Restart method restarts instance via Patroni POST API call.
|
||||||
|
func (p *Patroni) Restart(server *v1.Pod) error {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := json.NewEncoder(buf).Encode(map[string]interface{}{"restart_pending": true})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not encode json: %v", err)
|
||||||
|
}
|
||||||
|
apiURLString, err := apiURL(server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
status, err := p.GetStatus(server)
|
||||||
|
pending_restart, ok := status["pending_restart"]
|
||||||
|
if !ok || !pending_restart.(bool) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.httpPostOrPatch(http.MethodPost, apiURLString+restartPath, buf)
|
||||||
|
}
|
||||||
|
|
||||||
// GetMemberData read member data from patroni API
|
// GetMemberData read member data from patroni API
|
||||||
func (p *Patroni) GetMemberData(server *v1.Pod) (MemberData, error) {
|
func (p *Patroni) GetMemberData(server *v1.Pod) (MemberData, error) {
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue