Remove the check for the clone cluster name. (#270)

* Sanity checks for the cluster name, improve tests.

- check that the normal and clone cluster name complies with the valid
  service name. For clone cluster, only do it if clone timestamp is not
  set; with a clone timestamp set, the clone name points to the S3 bucket

 - add tests and improve existing ones, making sure we don't call Error()
   method for an empty error, as well as that we don't miss cases where
   expected error is not empty, but actual call to be tested does not
   return an error.

Code review by @zerg-junior and @Jan-M
This commit is contained in:
Oleksii Kliukin 2018-05-03 10:21:37 +02:00 committed by GitHub
parent fe47f9ebea
commit 4c8dfd7e20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 102 additions and 39 deletions

View File

@ -3,6 +3,7 @@ package spec
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
@ -76,6 +77,12 @@ const (
ClusterStatusInvalid PostgresStatus = "Invalid"
)
const (
serviceNameMaxLength = 63
clusterNameMaxLength = serviceNameMaxLength - len("-repl")
serviceNameRegexString = `^[a-z]([-a-z0-9]*[a-z0-9])?$`
)
// Postgresql defines PostgreSQL Custom Resource Definition Object.
type Postgresql struct {
metav1.TypeMeta `json:",inline"`
@ -126,7 +133,10 @@ type PostgresqlList struct {
Items []Postgresql `json:"items"`
}
var weekdays = map[string]int{"Sun": 0, "Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6}
var (
weekdays = map[string]int{"Sun": 0, "Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6}
serviceNameRegex = regexp.MustCompile(serviceNameRegexString)
)
func parseTime(s string) (time.Time, error) {
parts := strings.Split(s, ":")
@ -225,10 +235,31 @@ func extractClusterName(clusterName string, teamName string) (string, error) {
if strings.ToLower(clusterName[:teamNameLen+1]) != strings.ToLower(teamName)+"-" {
return "", fmt.Errorf("name must match {TEAM}-{NAME} format")
}
if len(clusterName) > clusterNameMaxLength {
return "", fmt.Errorf("name cannot be longer than %d characters", clusterNameMaxLength)
}
if !serviceNameRegex.MatchString(clusterName) {
return "", fmt.Errorf("name must confirm to DNS-1035, regex used for validation is %q",
serviceNameRegexString)
}
return clusterName[teamNameLen+1:], nil
}
func validateCloneClusterDescription(clone *CloneDescription) error {
// when cloning from the basebackup (no end timestamp) check that the cluster name is a valid service name
if clone.ClusterName != "" && clone.EndTimestamp == "" {
if !serviceNameRegex.MatchString(clone.ClusterName) {
return fmt.Errorf("clone cluster name must confirm to DNS-1035, regex used for validation is %q",
serviceNameRegexString)
}
if len(clone.ClusterName) > serviceNameMaxLength {
return fmt.Errorf("clone cluster name must be no longer than %d characters", serviceNameMaxLength)
}
}
return nil
}
type postgresqlListCopy PostgresqlList
type postgresqlCopy Postgresql
@ -252,22 +283,16 @@ func (p *Postgresql) UnmarshalJSON(data []byte) error {
}
tmp2 := Postgresql(tmp)
clusterName, err := extractClusterName(tmp2.ObjectMeta.Name, tmp2.Spec.TeamID)
if err == nil {
tmp2.Spec.ClusterName = clusterName
} else {
if clusterName, err := extractClusterName(tmp2.ObjectMeta.Name, tmp2.Spec.TeamID); err != nil {
tmp2.Error = err
tmp2.Status = ClusterStatusInvalid
} else if err := validateCloneClusterDescription(&tmp2.Spec.Clone); err != nil {
tmp2.Error = err
tmp2.Status = ClusterStatusInvalid
} else {
tmp2.Spec.ClusterName = clusterName
}
// The assumption below is that a cluster to clone, if any, belongs to the same team
if tmp2.Spec.Clone.ClusterName != "" {
_, err := extractClusterName(tmp2.Spec.Clone.ClusterName, tmp2.Spec.TeamID)
if err != nil {
tmp2.Error = fmt.Errorf("%s for the cluster to clone", err)
tmp2.Spec.Clone = CloneDescription{}
tmp2.Status = ClusterStatusInvalid
}
}
*p = tmp2
return nil

View File

@ -43,7 +43,10 @@ var clusterNames = []struct {
{"acid-test", "acid", "test", nil},
{"test-my-name", "test", "my-name", nil},
{"my-team-another-test", "my-team", "another-test", nil},
{"------strange-team-cluster", "-----", "strange-team-cluster", nil},
{"------strange-team-cluster", "-----", "strange-team-cluster",
errors.New(`name must confirm to DNS-1035, regex used for validation is "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)},
{"fooobar-fooobarfooobarfooobarfooobarfooobarfooobarfooobarfooobar", "fooobar", "",
errors.New("name cannot be longer than 58 characters")},
{"acid-test", "test", "", errors.New("name must match {TEAM}-{NAME} format")},
{"-test", "", "", errors.New("team name is empty")},
{"-test", "-", "", errors.New("name must match {TEAM}-{NAME} format")},
@ -51,6 +54,18 @@ var clusterNames = []struct {
{"-", "-", "", errors.New("name is too short")},
}
var cloneClusterDescriptions = []struct {
in *CloneDescription
err error
}{
{&CloneDescription{"foo+bar", "", "NotEmpty"}, nil},
{&CloneDescription{"foo+bar", "", ""},
errors.New(`clone cluster name must confirm to DNS-1035, regex used for validation is "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)},
{&CloneDescription{"foobar123456789012345678901234567890123456789012345678901234567890", "", ""},
errors.New("clone cluster name must be no longer than 63 characters")},
{&CloneDescription{"foobar", "", ""}, nil},
}
var maintenanceWindows = []struct {
in []byte
out MaintenanceWindow
@ -279,14 +294,15 @@ var unmarshalCluster = []struct {
Name: "acid-testcluster1",
},
Spec: PostgresSpec{
TeamID: "acid",
Clone: CloneDescription{},
TeamID: "acid",
Clone: CloneDescription{
ClusterName: "team-batman",
},
ClusterName: "testcluster1",
},
Status: ClusterStatusInvalid,
Error: errors.New("name must match {TEAM}-{NAME} format for the cluster to clone"),
Error: 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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":"Invalid"}`), 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},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}}}`), err: nil},
{[]byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1"`),
Postgresql{},
[]byte{},
@ -350,11 +366,12 @@ func TestParseTime(t *testing.T) {
for _, tt := range parseTimeTests {
aTime, err := parseTime(tt.in)
if err != nil {
if err.Error() != tt.err.Error() {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("ParseTime expected error: %v, got: %v", tt.err, err)
}
continue
} else if tt.err != nil {
t.Errorf("Expected error: %v", tt.err)
}
if aTime != tt.out {
@ -367,11 +384,12 @@ func TestWeekdayTime(t *testing.T) {
for _, tt := range parseWeekdayTests {
aTime, err := parseWeekday(tt.in)
if err != nil {
if err.Error() != tt.err.Error() {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("ParseWeekday expected error: %v, got: %v", tt.err, err)
}
continue
} else if tt.err != nil {
t.Errorf("Expected error: %v", tt.err)
}
if aTime != tt.out {
@ -383,9 +401,13 @@ func TestWeekdayTime(t *testing.T) {
func TestClusterName(t *testing.T) {
for _, tt := range clusterNames {
name, err := extractClusterName(tt.in, tt.inTeam)
if err != nil && err.Error() != tt.err.Error() {
t.Errorf("extractClusterName expected error: %v, got: %v", tt.err, err)
if err != nil {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("extractClusterName expected error: %v, got: %v", tt.err, err)
}
continue
} else if tt.err != nil {
t.Errorf("Expected error: %v", tt.err)
}
if name != tt.clusterName {
t.Errorf("Expected cluserName: %q, got: %q", tt.clusterName, name)
@ -393,17 +415,29 @@ func TestClusterName(t *testing.T) {
}
}
func TestCloneClusterDescription(t *testing.T) {
for _, tt := range cloneClusterDescriptions {
if err := validateCloneClusterDescription(tt.in); err != nil {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("testCloneClusterDescription expected error: %v, got: %v", tt.err, err)
}
} else if tt.err != nil {
t.Errorf("Expected error: %v", tt.err)
}
}
}
func TestUnmarshalMaintenanceWindow(t *testing.T) {
for _, tt := range maintenanceWindows {
var m MaintenanceWindow
err := m.UnmarshalJSON(tt.in)
if err != nil && err.Error() != tt.err.Error() {
t.Errorf("MaintenanceWindow unmarshal expected error: %v, got %v", tt.err, err)
continue
}
if tt.err != nil && err == nil {
t.Errorf("Expected error")
if err != nil {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("MaintenanceWindow 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(m, tt.out) {
@ -421,7 +455,6 @@ func TestMarshalMaintenanceWindow(t *testing.T) {
s, err := tt.out.MarshalJSON()
if err != nil {
t.Errorf("Marshal Error: %v", err)
continue
}
if !bytes.Equal(s, tt.in) {
@ -435,11 +468,12 @@ func TestPostgresUnmarshal(t *testing.T) {
var cluster Postgresql
err := cluster.UnmarshalJSON(tt.in)
if err != nil {
if err.Error() != tt.err.Error() {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("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(cluster, tt.out) {
@ -457,7 +491,6 @@ func TestMarshal(t *testing.T) {
m, err := json.Marshal(tt.out)
if err != nil {
t.Errorf("Marshal error: %v", err)
continue
}
if !bytes.Equal(m, tt.marshal) {
t.Errorf("Marshal Postgresql expected: %q, got: %q", string(tt.marshal), string(m))
@ -481,10 +514,15 @@ func TestUnmarshalPostgresList(t *testing.T) {
for _, tt := range postgresqlList {
var list PostgresqlList
err := list.UnmarshalJSON(tt.in)
if err != nil && err.Error() != tt.err.Error() {
t.Errorf("PostgresqlList unmarshal expected error: %v, got: %v", tt.err, err)
return
if err != nil {
if tt.err == nil || err.Error() != tt.err.Error() {
t.Errorf("PostgresqlList 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(list, tt.out) {
t.Errorf("Postgresql list unmarshall expected: %#v, got: %#v", tt.out, list)
}