Support major version upgrade via manifest and global upgrades via min version (#1372)

Support major version upgrade trigger via manifest. There is `off` `manual` and `full`. Manual is what you expect, and full will auto upgrade clusters below a certain threshold.
This commit is contained in:
Jan Mussler
2021-02-25 11:42:43 +01:00
committed by GitHub
parent ca968ca150
commit 636a9a8191
14 changed files with 331 additions and 62 deletions
+4
View File
@@ -206,6 +206,10 @@ type Config struct {
EnableLazySpiloUpgrade bool `name:"enable_lazy_spilo_upgrade" default:"false"`
EnablePgVersionEnvVar bool `name:"enable_pgversion_env_var" default:"true"`
EnableSpiloWalPathCompat bool `name:"enable_spilo_wal_path_compat" default:"false"`
MajorVersionUpgradeMode string `name:"major_version_upgrade_mode" default:"off"` // off - no actions, manual - manifest triggers action, full - manifest and minimal version violation trigger upgrade
MinimalMajorVersion string `name:"minimal_major_version" default:"9.5"`
TargetMajorVersion string `name:"target_major_version" default:"13"`
AllowedMajorUpgradeVersions []string `name:"allowed_major_upgrade_versions" default:"12,13"`
}
// MustMarshal marshals the config or panics
+11
View File
@@ -0,0 +1,11 @@
package httpclient
//go:generate mockgen -package mocks -destination=$PWD/mocks/$GOFILE -source=$GOFILE -build_flags=-mod=vendor
import "net/http"
// HTTPClient interface
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
Get(url string) (resp *http.Response, err error)
}
+39 -22
View File
@@ -3,7 +3,6 @@ package patroni
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
@@ -11,6 +10,8 @@ import (
"strconv"
"time"
httpclient "github.com/zalando/postgres-operator/pkg/util/httpclient"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
)
@@ -26,24 +27,28 @@ const (
type Interface interface {
Switchover(master *v1.Pod, candidate string) error
SetPostgresParameters(server *v1.Pod, options map[string]string) error
GetPatroniMemberState(pod *v1.Pod) (string, error)
GetMemberData(server *v1.Pod) (MemberData, error)
}
// Patroni API client
type Patroni struct {
httpClient *http.Client
httpClient httpclient.HTTPClient
logger *logrus.Entry
}
// New create patroni
func New(logger *logrus.Entry) *Patroni {
cl := http.Client{
Timeout: timeout,
func New(logger *logrus.Entry, client httpclient.HTTPClient) *Patroni {
if client == nil {
client = &http.Client{
Timeout: timeout,
}
}
return &Patroni{
logger: logger,
httpClient: &cl,
httpClient: client,
}
}
@@ -68,7 +73,9 @@ func (p *Patroni) httpPostOrPatch(method string, url string, body *bytes.Buffer)
return fmt.Errorf("could not create request: %v", err)
}
p.logger.Debugf("making %s http request: %s", method, request.URL.String())
if p.logger != nil {
p.logger.Debugf("making %s http request: %s", method, request.URL.String())
}
resp, err := p.httpClient.Do(request)
if err != nil {
@@ -126,35 +133,45 @@ func (p *Patroni) SetPostgresParameters(server *v1.Pod, parameters map[string]st
return p.httpPostOrPatch(http.MethodPatch, apiURLString+configPath, buf)
}
//GetPatroniMemberState returns a state of member of a Patroni cluster
func (p *Patroni) GetPatroniMemberState(server *v1.Pod) (string, error) {
// MemberDataPatroni child element
type MemberDataPatroni struct {
Version string `json:"version"`
Scope string `json:"scope"`
}
// MemberData Patroni member data from Patroni API
type MemberData struct {
State string `json:"state"`
Role string `json:"role"`
ServerVersion int `json:"server_version"`
PendingRestart bool `json:"pending_restart"`
ClusterUnlocked bool `json:"cluster_unlocked"`
Patroni MemberDataPatroni `json:"patroni"`
}
// GetMemberData read member data from patroni API
func (p *Patroni) GetMemberData(server *v1.Pod) (MemberData, error) {
apiURLString, err := apiURL(server)
if err != nil {
return "", err
return MemberData{}, err
}
response, err := p.httpClient.Get(apiURLString)
if err != nil {
return "", fmt.Errorf("could not perform Get request: %v", err)
return MemberData{}, fmt.Errorf("could not perform Get request: %v", err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("could not read response: %v", err)
return MemberData{}, fmt.Errorf("could not read response: %v", err)
}
data := make(map[string]interface{})
data := MemberData{}
err = json.Unmarshal(body, &data)
if err != nil {
return "", err
return MemberData{}, err
}
state, ok := data["state"].(string)
if !ok {
return "", errors.New("Patroni Get call response contains wrong type for 'state' field")
}
return state, nil
return data, nil
}
+37 -1
View File
@@ -1,10 +1,17 @@
package patroni
import (
"bytes"
"errors"
"fmt"
"k8s.io/api/core/v1"
"io/ioutil"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/zalando/postgres-operator/mocks"
v1 "k8s.io/api/core/v1"
)
func newMockPod(ip string) *v1.Pod {
@@ -72,3 +79,32 @@ func TestApiURL(t *testing.T) {
}
}
}
func TestPatroniAPI(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
json := `{"state": "running", "postmaster_start_time": "2021-02-19 14:31:50.053 CET", "role": "master", "server_version": 90621, "cluster_unlocked": false, "xlog": {"location": 55978296057856}, "timeline": 6, "database_system_identifier": "6462555844314089962", "pending_restart": true, "patroni": {"version": "2.0.1", "scope": "acid-rest92-standby"}}`
r := ioutil.NopCloser(bytes.NewReader([]byte(json)))
response := http.Response{
Status: "200",
Body: r,
}
mockClient := mocks.NewMockHTTPClient(ctrl)
mockClient.EXPECT().Get(gomock.Any()).Return(&response, nil)
p := New(nil, mockClient)
pod := v1.Pod{
Status: v1.PodStatus{
PodIP: "192.168.100.1",
},
}
_, err := p.GetMemberData(&pod)
if err != nil {
t.Errorf("Could not read Patroni data: %v", err)
}
}