Use /status subresource instead of plain manifest field (#534)

* turns PostgresStatus type into a struct with field PostgresClusterStatus
* setStatus patch target is now /status subresource
* unmarshalling PostgresStatus takes care of previous status field convention
* new simple bool functions status.Running(), status.Creating()
This commit is contained in:
Felix Kunde 2019-05-07 12:01:45 +02:00 committed by GitHub
parent 25e02ad755
commit 0fbfbb23bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 356 additions and 205 deletions

View File

@ -13,6 +13,7 @@ rules:
- acid.zalan.do - acid.zalan.do
resources: resources:
- postgresqls - postgresqls
- postgresqls/status
- operatorconfigurations - operatorconfigurations
verbs: verbs:
- "*" - "*"

View File

@ -14,6 +14,7 @@ rules:
- acid.zalan.do - acid.zalan.do
resources: resources:
- postgresqls - postgresqls
- postgresqls/status
- operatorconfigurations - operatorconfigurations
verbs: verbs:
- "*" - "*"

View File

@ -2,14 +2,14 @@ package v1
// ClusterStatusUnknown etc : status of a Postgres cluster known to the operator // ClusterStatusUnknown etc : status of a Postgres cluster known to the operator
const ( const (
ClusterStatusUnknown PostgresStatus = "" ClusterStatusUnknown = ""
ClusterStatusCreating PostgresStatus = "Creating" ClusterStatusCreating = "Creating"
ClusterStatusUpdating PostgresStatus = "Updating" ClusterStatusUpdating = "Updating"
ClusterStatusUpdateFailed PostgresStatus = "UpdateFailed" ClusterStatusUpdateFailed = "UpdateFailed"
ClusterStatusSyncFailed PostgresStatus = "SyncFailed" ClusterStatusSyncFailed = "SyncFailed"
ClusterStatusAddFailed PostgresStatus = "CreateFailed" ClusterStatusAddFailed = "CreateFailed"
ClusterStatusRunning PostgresStatus = "Running" ClusterStatusRunning = "Running"
ClusterStatusInvalid PostgresStatus = "Invalid" ClusterStatusInvalid = "Invalid"
) )
const ( const (

View File

@ -8,6 +8,7 @@ import (
) )
type postgresqlCopy Postgresql type postgresqlCopy Postgresql
type postgresStatusCopy PostgresStatus
// MarshalJSON converts a maintenance window definition to JSON. // MarshalJSON converts a maintenance window definition to JSON.
func (m *MaintenanceWindow) MarshalJSON() ([]byte, error) { func (m *MaintenanceWindow) MarshalJSON() ([]byte, error) {
@ -69,6 +70,26 @@ func (m *MaintenanceWindow) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// UnmarshalJSON converts a JSON to the status subresource definition.
func (ps *PostgresStatus) UnmarshalJSON(data []byte) error {
var (
tmp postgresStatusCopy
status string
)
err := json.Unmarshal(data, &tmp)
if err != nil {
metaErr := json.Unmarshal(data, &status)
if metaErr != nil {
return fmt.Errorf("Could not parse status: %v; err %v", string(data), metaErr)
}
tmp.PostgresClusterStatus = status
}
*ps = PostgresStatus(tmp)
return nil
}
// UnmarshalJSON converts a JSON into the PostgreSQL object. // UnmarshalJSON converts a JSON into the PostgreSQL object.
func (p *Postgresql) UnmarshalJSON(data []byte) error { func (p *Postgresql) UnmarshalJSON(data []byte) error {
var tmp postgresqlCopy var tmp postgresqlCopy
@ -81,7 +102,7 @@ func (p *Postgresql) UnmarshalJSON(data []byte) error {
} }
tmp.Error = err.Error() tmp.Error = err.Error()
tmp.Status = ClusterStatusInvalid tmp.Status = PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid}
*p = Postgresql(tmp) *p = Postgresql(tmp)
@ -91,10 +112,10 @@ func (p *Postgresql) UnmarshalJSON(data []byte) error {
if clusterName, err := extractClusterName(tmp2.ObjectMeta.Name, tmp2.Spec.TeamID); err != nil { if clusterName, err := extractClusterName(tmp2.ObjectMeta.Name, tmp2.Spec.TeamID); err != nil {
tmp2.Error = err.Error() tmp2.Error = err.Error()
tmp2.Status = ClusterStatusInvalid tmp2.Status = PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid}
} else if err := validateCloneClusterDescription(&tmp2.Spec.Clone); err != nil { } else if err := validateCloneClusterDescription(&tmp2.Spec.Clone); err != nil {
tmp2.Error = err.Error() tmp2.Error = err.Error()
tmp2.Status = ClusterStatusInvalid tmp2.Status = PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid}
} else { } else {
tmp2.Spec.ClusterName = clusterName tmp2.Spec.ClusterName = clusterName
} }

View File

@ -16,7 +16,7 @@ type Postgresql struct {
metav1.ObjectMeta `json:"metadata,omitempty"` metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PostgresSpec `json:"spec"` Spec PostgresSpec `json:"spec"`
Status PostgresStatus `json:"status,omitempty"` Status PostgresStatus `json:"status"`
Error string `json:"-"` Error string `json:"-"`
} }
@ -129,4 +129,6 @@ type Sidecar struct {
type UserFlags []string type UserFlags []string
// PostgresStatus contains status of the PostgreSQL cluster (running, creation failed etc.) // PostgresStatus contains status of the PostgreSQL cluster (running, creation failed etc.)
type PostgresStatus string type PostgresStatus struct {
PostgresClusterStatus string `json:"PostgresClusterStatus"`
}

View File

@ -85,12 +85,22 @@ func validateCloneClusterDescription(clone *CloneDescription) error {
} }
// Success of the current Status // Success of the current Status
func (status PostgresStatus) Success() bool { func (postgresStatus PostgresStatus) Success() bool {
return status != ClusterStatusAddFailed && return postgresStatus.PostgresClusterStatus != ClusterStatusAddFailed &&
status != ClusterStatusUpdateFailed && postgresStatus.PostgresClusterStatus != ClusterStatusUpdateFailed &&
status != ClusterStatusSyncFailed postgresStatus.PostgresClusterStatus != ClusterStatusSyncFailed
} }
func (status PostgresStatus) String() string { // Running status of cluster
return string(status) func (postgresStatus PostgresStatus) Running() bool {
return postgresStatus.PostgresClusterStatus == ClusterStatusRunning
}
// Creating status of cluster
func (postgresStatus PostgresStatus) Creating() bool {
return postgresStatus.PostgresClusterStatus == ClusterStatusCreating
}
func (postgresStatus PostgresStatus) String() string {
return postgresStatus.PostgresClusterStatus
} }

View File

@ -111,16 +111,32 @@ var maintenanceWindows = []struct {
{[]byte(`"Mon:00:00"`), MaintenanceWindow{}, errors.New("incorrect maintenance window format")}, {[]byte(`"Mon:00:00"`), MaintenanceWindow{}, errors.New("incorrect maintenance window format")},
{[]byte(`"Mon:00:00-00:00:00"`), MaintenanceWindow{}, errors.New("could not parse end time: incorrect time format")}} {[]byte(`"Mon:00:00-00:00:00"`), MaintenanceWindow{}, errors.New("could not parse end time: incorrect time format")}}
var postgresStatus = []struct {
in []byte
out PostgresStatus
err error
}{
{[]byte(`{"PostgresClusterStatus":"Running"}`),
PostgresStatus{PostgresClusterStatus: ClusterStatusRunning}, nil},
{[]byte(`{"PostgresClusterStatus":""}`),
PostgresStatus{PostgresClusterStatus: ClusterStatusUnknown}, nil},
{[]byte(`"Running"`),
PostgresStatus{PostgresClusterStatus: ClusterStatusRunning}, nil},
{[]byte(`""`),
PostgresStatus{PostgresClusterStatus: ClusterStatusUnknown}, nil}}
var unmarshalCluster = []struct { var unmarshalCluster = []struct {
in []byte in []byte
out Postgresql out Postgresql
marshal []byte marshal []byte
err error err error
}{{ }{
[]byte(`{ // example with simple status field
{
in: []byte(`{
"kind": "Postgresql","apiVersion": "acid.zalan.do/v1", "kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), "metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`),
Postgresql{ out: Postgresql{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "Postgresql", Kind: "Postgresql",
APIVersion: "acid.zalan.do/v1", APIVersion: "acid.zalan.do/v1",
@ -128,12 +144,34 @@ var unmarshalCluster = []struct {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "acid-testcluster1", Name: "acid-testcluster1",
}, },
Status: ClusterStatusInvalid, Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid},
// This error message can vary between Go versions, so compute it for the current version. // This error message can vary between Go versions, so compute it for the current version.
Error: json.Unmarshal([]byte(`{"teamId": 0}`), &PostgresSpec{}).Error(), Error: json.Unmarshal([]byte(`{"teamId": 0}`), &PostgresSpec{}).Error(),
}, },
[]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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":"Invalid"}`), nil}, 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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":"Invalid"}`),
{[]byte(`{ err: nil},
// example with /status subresource
{
in: []byte(`{
"kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`),
out: Postgresql{
TypeMeta: metav1.TypeMeta{
Kind: "Postgresql",
APIVersion: "acid.zalan.do/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "acid-testcluster1",
},
Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid},
// This error message can vary between Go versions, so compute it for the current version.
Error: json.Unmarshal([]byte(`{"teamId": 0}`), &PostgresSpec{}).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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":{"PostgresClusterStatus":"Invalid"}}`),
err: nil},
// example with detailed input manifest
{
in: []byte(`{
"kind": "Postgresql", "kind": "Postgresql",
"apiVersion": "acid.zalan.do/v1", "apiVersion": "acid.zalan.do/v1",
"metadata": { "metadata": {
@ -204,8 +242,8 @@ var unmarshalCluster = []struct {
"05:00-05:15" "05:00-05:15"
] ]
} }
}`), }`),
Postgresql{ out: Postgresql{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "Postgresql", Kind: "Postgresql",
APIVersion: "acid.zalan.do/v1", APIVersion: "acid.zalan.do/v1",
@ -273,10 +311,12 @@ var unmarshalCluster = []struct {
}, },
Error: "", Error: "",
}, },
[]byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"9.6","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"volume":{"size":"5Gi","storageClass":"SSD"},"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"}}}`), nil}, marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"9.6","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"volume":{"size":"5Gi","storageClass":"SSD"},"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},
// example with teamId set in input
{ {
[]byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "teapot-testcluster1"}, "spec": {"teamId": "acid"}}`), in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "teapot-testcluster1"}, "spec": {"teamId": "acid"}}`),
Postgresql{ out: Postgresql{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "Postgresql", Kind: "Postgresql",
APIVersion: "acid.zalan.do/v1", APIVersion: "acid.zalan.do/v1",
@ -285,10 +325,12 @@ var unmarshalCluster = []struct {
Name: "teapot-testcluster1", Name: "teapot-testcluster1",
}, },
Spec: PostgresSpec{TeamID: "acid"}, Spec: PostgresSpec{TeamID: "acid"},
Status: ClusterStatusInvalid, Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid},
Error: errors.New("name must match {TEAM}-{NAME} format").Error(), Error: errors.New("name must match {TEAM}-{NAME} format").Error(),
}, },
[]byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"teapot-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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":"Invalid"}`), nil}, marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"teapot-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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":{"PostgresClusterStatus":"Invalid"}}`),
err: nil},
// clone example
{ {
in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "clone": {"cluster": "team-batman"}}}`), in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "clone": {"cluster": "team-batman"}}}`),
out: Postgresql{ out: Postgresql{
@ -308,22 +350,26 @@ var unmarshalCluster = []struct {
}, },
Error: "", 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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}}}`), err: nil}, 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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}},"status":{"PostgresClusterStatus":""}}`),
{[]byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1"`), err: nil},
Postgresql{}, // erroneous examples
[]byte{}, {
errors.New("unexpected end of JSON input")}, in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1"`),
{[]byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster","creationTimestamp":qaz},"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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":"Invalid"}`), out: Postgresql{},
Postgresql{}, marshal: []byte{},
[]byte{}, err: errors.New("unexpected end of JSON input")},
errors.New("invalid character 'q' looking for beginning of value")}} {
in: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster","creationTimestamp":qaz},"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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":{"PostgresClusterStatus":"Invalid"}}`),
out: Postgresql{},
marshal: []byte{},
err: errors.New("invalid character 'q' looking for beginning of value")}}
var postgresqlList = []struct { var postgresqlList = []struct {
in []byte in []byte
out PostgresqlList out PostgresqlList
err error err error
}{ }{
{[]byte(`{"apiVersion":"v1","items":[{"apiVersion":"acid.zalan.do/v1","kind":"Postgresql","metadata":{"labels":{"team":"acid"},"name":"acid-testcluster42","namespace":"default","resourceVersion":"30446957","selfLink":"/apis/acid.zalan.do/v1/namespaces/default/postgresqls/acid-testcluster42","uid":"857cd208-33dc-11e7-b20a-0699041e4b03"},"spec":{"allowedSourceRanges":["185.85.220.0/22"],"numberOfInstances":1,"postgresql":{"version":"9.6"},"teamId":"acid","volume":{"size":"10Gi"}},"status":"Running"}],"kind":"List","metadata":{},"resourceVersion":"","selfLink":""}`), {[]byte(`{"apiVersion":"v1","items":[{"apiVersion":"acid.zalan.do/v1","kind":"Postgresql","metadata":{"labels":{"team":"acid"},"name":"acid-testcluster42","namespace":"default","resourceVersion":"30446957","selfLink":"/apis/acid.zalan.do/v1/namespaces/default/postgresqls/acid-testcluster42","uid":"857cd208-33dc-11e7-b20a-0699041e4b03"},"spec":{"allowedSourceRanges":["185.85.220.0/22"],"numberOfInstances":1,"postgresql":{"version":"9.6"},"teamId":"acid","volume":{"size":"10Gi"}},"status":{"PostgresClusterStatus":"Running"}}],"kind":"List","metadata":{},"resourceVersion":"","selfLink":""}`),
PostgresqlList{ PostgresqlList{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "List", Kind: "List",
@ -350,7 +396,9 @@ var postgresqlList = []struct {
AllowedSourceRanges: []string{"185.85.220.0/22"}, AllowedSourceRanges: []string{"185.85.220.0/22"},
NumberOfInstances: 1, NumberOfInstances: 1,
}, },
Status: ClusterStatusRunning, Status: PostgresStatus{
PostgresClusterStatus: ClusterStatusRunning,
},
Error: "", Error: "",
}}, }},
}, },
@ -469,6 +517,25 @@ func TestMarshalMaintenanceWindow(t *testing.T) {
} }
} }
func TestUnmarshalPostgresStatus(t *testing.T) {
for _, tt := range postgresStatus {
var ps PostgresStatus
err := ps.UnmarshalJSON(tt.in)
if err != nil {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("CR status unmarshal expected error: %v, got %v", tt.err, err)
}
continue
//} else if tt.err != nil {
//t.Errorf("Expected error: %v", tt.err)
}
if !reflect.DeepEqual(ps, tt.out) {
t.Errorf("Expected status: %#v, got: %#v", tt.out, ps)
}
}
}
func TestPostgresUnmarshal(t *testing.T) { func TestPostgresUnmarshal(t *testing.T) {
for _, tt := range unmarshalCluster { for _, tt := range unmarshalCluster {
var cluster Postgresql var cluster Postgresql
@ -494,12 +561,26 @@ func TestMarshal(t *testing.T) {
continue continue
} }
// Unmarshal and marshal example to capture api changes
var cluster Postgresql
err := cluster.UnmarshalJSON(tt.marshal)
if err != nil {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("Backwards compatibility unmarshal expected error: %v, got: %v", tt.err, err)
}
continue
}
expected, err := json.Marshal(cluster)
if err != nil {
t.Errorf("Backwards compatibility marshal error: %v", err)
}
m, err := json.Marshal(tt.out) m, err := json.Marshal(tt.out)
if err != nil { if err != nil {
t.Errorf("Marshal error: %v", err) t.Errorf("Marshal error: %v", err)
} }
if !bytes.Equal(m, tt.marshal) { if !bytes.Equal(m, expected) {
t.Errorf("Marshal Postgresql \nexpected: %q, \ngot: %q", string(tt.marshal), string(m)) t.Errorf("Marshal Postgresql \nexpected: %q, \ngot: %q", string(expected), string(m))
} }
} }
} }

View File

@ -479,6 +479,22 @@ func (in *PostgresSpec) DeepCopy() *PostgresSpec {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgresStatus) DeepCopyInto(out *PostgresStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgresStatus.
func (in *PostgresStatus) DeepCopy() *PostgresStatus {
if in == nil {
return nil
}
out := new(PostgresStatus)
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 *PostgresUsersConfiguration) DeepCopyInto(out *PostgresUsersConfiguration) { func (in *PostgresUsersConfiguration) DeepCopyInto(out *PostgresUsersConfiguration) {
*out = *in *out = *in
@ -501,6 +517,7 @@ func (in *Postgresql) DeepCopyInto(out *Postgresql) {
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec) in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
return return
} }

View File

@ -4,6 +4,7 @@ package cluster
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
@ -19,8 +20,6 @@ import (
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"encoding/json"
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"
"github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util"
@ -149,21 +148,24 @@ func (c *Cluster) setProcessName(procName string, args ...interface{}) {
} }
} }
func (c *Cluster) setStatus(status acidv1.PostgresStatus) { // SetStatus of Postgres cluster
// TODO: eventually switch to updateStatus() for kubernetes 1.11 and above // TODO: eventually switch to updateStatus() for kubernetes 1.11 and above
var ( func (c *Cluster) setStatus(status string) {
err error var pgStatus acidv1.PostgresStatus
b []byte pgStatus.PostgresClusterStatus = status
)
if b, err = json.Marshal(status); err != nil { patch, err := json.Marshal(struct {
PgStatus interface{} `json:"status"`
}{&pgStatus})
if err != nil {
c.logger.Errorf("could not marshal status: %v", err) c.logger.Errorf("could not marshal status: %v", err)
} }
patch := []byte(fmt.Sprintf(`{"status": %s}`, string(b)))
// we cannot do a full scale update here without fetching the previous manifest (as the resourceVersion may differ), // 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 Kubernets 1.11) // however, we could do patch without it. In the future, once /status subresource is there (starting Kubernets 1.11)
// we should take advantage of it. // we should take advantage of it.
newspec, err := c.KubeClient.AcidV1ClientSet.AcidV1().Postgresqls(c.clusterNamespace()).Patch(c.Name, types.MergePatchType, patch) newspec, err := c.KubeClient.AcidV1ClientSet.AcidV1().Postgresqls(c.clusterNamespace()).Patch(c.Name, types.MergePatchType, patch, "status")
if err != nil { if err != nil {
c.logger.Errorf("could not update status: %v", err) c.logger.Errorf("could not update status: %v", err)
} }
@ -172,7 +174,7 @@ func (c *Cluster) setStatus(status acidv1.PostgresStatus) {
} }
func (c *Cluster) isNewCluster() bool { func (c *Cluster) isNewCluster() bool {
return c.Status == acidv1.ClusterStatusCreating return c.Status.Creating()
} }
// initUsers populates c.systemUsers and c.pgUsers maps. // initUsers populates c.systemUsers and c.pgUsers maps.

View File

@ -20,10 +20,20 @@ const (
) )
var logger = logrus.New().WithField("test", "cluster") var logger = logrus.New().WithField("test", "cluster")
var cl = New(Config{OpConfig: config.Config{ProtectedRoles: []string{"admin"}, var cl = New(
Auth: config.Auth{SuperUsername: superUserName, Config{
ReplicationUsername: replicationUserName}}}, OpConfig: config.Config{
k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger) ProtectedRoles: []string{"admin"},
Auth: config.Auth{
SuperUsername: superUserName,
ReplicationUsername: replicationUserName,
},
},
},
k8sutil.NewMockKubernetesClient(),
acidv1.Postgresql{},
logger,
)
func TestInitRobotUsers(t *testing.T) { func TestInitRobotUsers(t *testing.T) {
testName := "TestInitRobotUsers" testName := "TestInitRobotUsers"

View File

@ -28,7 +28,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
if err != nil { if err != nil {
c.logger.Warningf("error while syncing cluster state: %v", err) c.logger.Warningf("error while syncing cluster state: %v", err)
c.setStatus(acidv1.ClusterStatusSyncFailed) c.setStatus(acidv1.ClusterStatusSyncFailed)
} else if c.Status != acidv1.ClusterStatusRunning { } else if !c.Status.Running() {
c.setStatus(acidv1.ClusterStatusRunning) c.setStatus(acidv1.ClusterStatusRunning)
} }
}() }()

View File

@ -6,82 +6,24 @@ import (
"testing" "testing"
b64 "encoding/base64" b64 "encoding/base64"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/k8sutil"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
const ( const (
testInfrastructureRolesSecretName = "infrastructureroles-test" testInfrastructureRolesSecretName = "infrastructureroles-test"
) )
type mockSecret struct {
v1core.SecretInterface
}
type mockConfigMap struct {
v1core.ConfigMapInterface
}
func (c *mockSecret) Get(name string, options metav1.GetOptions) (*v1.Secret, error) {
if name != testInfrastructureRolesSecretName {
return nil, fmt.Errorf("NotFound")
}
secret := &v1.Secret{}
secret.Name = mockController.opConfig.ClusterNameLabel
secret.Data = map[string][]byte{
"user1": []byte("testrole"),
"password1": []byte("testpassword"),
"inrole1": []byte("testinrole"),
"foobar": []byte(b64.StdEncoding.EncodeToString([]byte("password"))),
}
return secret, nil
}
func (c *mockConfigMap) Get(name string, options metav1.GetOptions) (*v1.ConfigMap, error) {
if name != testInfrastructureRolesSecretName {
return nil, fmt.Errorf("NotFound")
}
configmap := &v1.ConfigMap{}
configmap.Name = mockController.opConfig.ClusterNameLabel
configmap.Data = map[string]string{
"foobar": "{}",
}
return configmap, nil
}
type MockSecretGetter struct {
}
type MockConfigMapsGetter struct {
}
func (c *MockSecretGetter) Secrets(namespace string) v1core.SecretInterface {
return &mockSecret{}
}
func (c *MockConfigMapsGetter) ConfigMaps(namespace string) v1core.ConfigMapInterface {
return &mockConfigMap{}
}
func newMockKubernetesClient() k8sutil.KubernetesClient {
return k8sutil.KubernetesClient{
SecretsGetter: &MockSecretGetter{},
ConfigMapsGetter: &MockConfigMapsGetter{},
}
}
func newMockController() *Controller { func newMockController() *Controller {
controller := NewController(&spec.ControllerConfig{}) controller := NewController(&spec.ControllerConfig{})
controller.opConfig.ClusterNameLabel = "cluster-name" controller.opConfig.ClusterNameLabel = "cluster-name"
controller.opConfig.InfrastructureRolesSecretName = controller.opConfig.InfrastructureRolesSecretName =
spec.NamespacedName{Namespace: v1.NamespaceDefault, Name: testInfrastructureRolesSecretName} spec.NamespacedName{Namespace: v1.NamespaceDefault, Name: testInfrastructureRolesSecretName}
controller.opConfig.Workers = 4 controller.opConfig.Workers = 4
controller.KubeClient = newMockKubernetesClient() controller.KubeClient = k8sutil.NewMockKubernetesClient()
return controller return controller
} }

View File

@ -2,6 +2,10 @@ package k8sutil
import ( import (
"fmt" "fmt"
"reflect"
b64 "encoding/base64"
"github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/constants"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
policybeta1 "k8s.io/api/policy/v1beta1" policybeta1 "k8s.io/api/policy/v1beta1"
@ -15,9 +19,9 @@ import (
rbacv1beta1 "k8s.io/client-go/kubernetes/typed/rbac/v1beta1" rbacv1beta1 "k8s.io/client-go/kubernetes/typed/rbac/v1beta1"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"reflect"
acidv1client "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned" acidv1client "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
// KubernetesClient describes getters for Kubernetes objects // KubernetesClient describes getters for Kubernetes objects
@ -41,6 +45,20 @@ type KubernetesClient struct {
AcidV1ClientSet *acidv1client.Clientset AcidV1ClientSet *acidv1client.Clientset
} }
type mockSecret struct {
v1core.SecretInterface
}
type MockSecretGetter struct {
}
type mockConfigMap struct {
v1core.ConfigMapInterface
}
type MockConfigMapsGetter struct {
}
// RestConfig creates REST config // RestConfig creates REST config
func RestConfig(kubeConfig string, outOfCluster bool) (*rest.Config, error) { func RestConfig(kubeConfig string, outOfCluster bool) (*rest.Config, error) {
if outOfCluster { if outOfCluster {
@ -140,3 +158,49 @@ func SamePDB(cur, new *policybeta1.PodDisruptionBudget) (match bool, reason stri
return return
} }
func (c *mockSecret) Get(name string, options metav1.GetOptions) (*v1.Secret, error) {
if name != "infrastructureroles-test" {
return nil, fmt.Errorf("NotFound")
}
secret := &v1.Secret{}
secret.Name = "testcluster"
secret.Data = map[string][]byte{
"user1": []byte("testrole"),
"password1": []byte("testpassword"),
"inrole1": []byte("testinrole"),
"foobar": []byte(b64.StdEncoding.EncodeToString([]byte("password"))),
}
return secret, nil
}
func (c *mockConfigMap) Get(name string, options metav1.GetOptions) (*v1.ConfigMap, error) {
if name != "infrastructureroles-test" {
return nil, fmt.Errorf("NotFound")
}
configmap := &v1.ConfigMap{}
configmap.Name = "testcluster"
configmap.Data = map[string]string{
"foobar": "{}",
}
return configmap, nil
}
// Secrets to be mocked
func (c *MockSecretGetter) Secrets(namespace string) v1core.SecretInterface {
return &mockSecret{}
}
// ConfigMaps to be mocked
func (c *MockConfigMapsGetter) ConfigMaps(namespace string) v1core.ConfigMapInterface {
return &mockConfigMap{}
}
// NewMockKubernetesClient for other tests
func NewMockKubernetesClient() KubernetesClient {
return KubernetesClient{
SecretsGetter: &MockSecretGetter{},
ConfigMapsGetter: &MockConfigMapsGetter{},
}
}