Skip to content

Remove the check for the clone cluster name. #270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pkg/cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ func TestInitRobotUsers(t *testing.T) {
{
manifestUsers: map[string]spec.UserFlags{"foo": {"superuser", "createdb"}},
infraRoles: map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Password: "bar"}},
result: map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Password: "bar"}},
err: nil,
result: map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Password: "bar"}},
err: nil,
},
{
manifestUsers: map[string]spec.UserFlags{"!fooBar": {"superuser", "createdb"}},
Expand Down
53 changes: 39 additions & 14 deletions pkg/spec/postgresql.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package spec
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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, ":")
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do existing cluster clones always have names adhering to this format ? that should be enforced by k8s, right ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

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

Expand All @@ -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 {
Copy link
Member

@Jan-M Jan-M Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this code now support restoring from S3 where the cluster name is basically: "foobar" (spilo ec2 deployments)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That line doesn't validate the clone cluster name, it happens at https://github.com/zalando-incubator/postgres-operator/pull/270/files/04d6a7607508fef669faacfb57ef9ef443b587da#diff-44fe837275fc3b64cc6c85eb90ce74c9R247

There, the name for the cluster clone that comes from S3 is not validated.

tmp2.Error = err
tmp2.Status = ClusterStatusInvalid
} else if err := validateCloneClusterDescription(&tmp2.Spec.Clone); err != nil {
tmp2.Error = err
tmp2.Status = ClusterStatusInvalid
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel the summary of this discussion is worth a comment here:

anyone able to deploy a manifest can clone from any cluster's backups, and thus read all the data in other clusters.

It pretty much depends on how different teams are separated on the infrastructure level;

i.e. it is outside of the operator scope to check this

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
Expand Down
88 changes: 63 additions & 25 deletions pkg/spec/postgresql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,29 @@ var clusterNames = []struct {
{"acid-test", "acid", "test", nil},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like "foobar" is not covered but should work as this is team "foo" and cluster "bar" in ec2 spilo way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jan-M which test do you have in mind? We do test with exactly 'foobar' as a clone cluster name for cloning from basebackup in line 66 of this patch.

{"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")},
{"", "-", "", errors.New("name is too short")},
{"-", "-", "", 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
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -383,27 +401,43 @@ 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)
}
}
}

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) {
Expand All @@ -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) {
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and go no error, right ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

}

if !reflect.DeepEqual(cluster, tt.out) {
Expand All @@ -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))
Expand All @@ -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)
}
Expand Down