Merge branch 'master' into sketch-e2e-tests-update-client

This commit is contained in:
erthalion 2019-05-07 14:29:25 +02:00
commit 42f5ac292f
14 changed files with 363 additions and 209 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

@ -32,16 +32,19 @@ kubectl create -f manifests/postgres-operator.yaml # deployment
## Helm chart ## Helm chart
Another possibility is using a provided [Helm](https://helm.sh/) chart which Alternatively, the operator can be installed by using the provided [Helm](https://helm.sh/)
saves you these steps. Therefore, you would need to install the helm CLI on your chart which saves you the manual steps. Therefore, you would need to install
machine. After initializing helm (and its server component Tiller) in your local the helm CLI on your machine. After initializing helm (and its server
cluster you can install the operator chart. component Tiller) in your local cluster you can install the operator chart.
You can define a release name that is prepended to the operator resource's
names. Use `--name zalando` to match with the default service account name
as older operator versions do not support custom names for service accounts.
```bash ```bash
# 1) initialize helm # 1) initialize helm
helm init helm init
# 2) install postgres-operator chart # 2) install postgres-operator chart
helm install --name postgres-operator ./charts/postgres-operator helm install --name zalando ./charts/postgres-operator
``` ```
## Create a Postgres cluster ## Create a Postgres cluster

View File

@ -7,6 +7,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": {
@ -205,7 +243,7 @@ var unmarshalCluster = []struct {
] ]
} }
}`), }`),
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{},
}
}