Merge branch 'master' into feature/tests
# Conflicts: # pkg/spec/postgresql.go
This commit is contained in:
		
						commit
						9086beaa40
					
				|  | @ -43,4 +43,4 @@ spec: | |||
|     maximum_lag_on_failover: 33554432 | ||||
|   maintenanceWindows: | ||||
|   - 01:00-06:00 #UTC | ||||
|   - Sat:00:00-Sat:04:00 | ||||
|   - Sat:00:00-04:00 | ||||
|  |  | |||
|  | @ -105,11 +105,41 @@ func (c *Cluster) logVolumeChanges(old, new spec.Volume, reason string) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *Cluster) getOAuthToken() (string, error) { | ||||
| 	//TODO: we can move this function to the Controller in case it will be needed there. As for now we use it only in the Cluster
 | ||||
| 	// Temporary getting postgresql-operator secret from the NamespaceDefault
 | ||||
| 	credentialsSecret, err := c.KubeClient. | ||||
| 		Secrets(c.OpConfig.OAuthTokenSecretName.Namespace). | ||||
| 		Get(c.OpConfig.OAuthTokenSecretName.Name) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		c.logger.Debugf("Oauth token secret name: %s", c.OpConfig.OAuthTokenSecretName) | ||||
| 		return "", fmt.Errorf("could not get credentials secret: %v", err) | ||||
| 	} | ||||
| 	data := credentialsSecret.Data | ||||
| 
 | ||||
| 	if string(data["read-only-token-type"]) != "Bearer" { | ||||
| 		return "", fmt.Errorf("wrong token type: %v", data["read-only-token-type"]) | ||||
| 	} | ||||
| 
 | ||||
| 	return string(data["read-only-token-secret"]), nil | ||||
| } | ||||
| 
 | ||||
| func (c *Cluster) getTeamMembers() ([]string, error) { | ||||
| 	if c.Spec.TeamID == "" { | ||||
| 		return nil, fmt.Errorf("no teamId specified") | ||||
| 	} | ||||
| 	teamInfo, err := c.TeamsAPIClient.TeamInfo(c.Spec.TeamID) | ||||
| 	if !c.OpConfig.EnableTeamsAPI { | ||||
| 		c.logger.Debug("Team API is disabled, returning empty list of members") | ||||
| 		return []string{}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	token, err := c.getOAuthToken() | ||||
| 	if err != nil { | ||||
| 		return []string{}, fmt.Errorf("could not get oauth token: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	teamInfo, err := c.TeamsAPIClient.TeamInfo(c.Spec.TeamID, token) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not get team info: %v", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -47,7 +47,8 @@ func New(controllerConfig *Config, operatorConfig *config.Config) *Controller { | |||
| 		logger.Level = logrus.DebugLevel | ||||
| 	} | ||||
| 
 | ||||
| 	controllerConfig.TeamsAPIClient = teams.NewTeamsAPI(operatorConfig.TeamsAPIUrl, logger, operatorConfig.EnableTeamsAPI) | ||||
| 	controllerConfig.TeamsAPIClient = teams.NewTeamsAPI(operatorConfig.TeamsAPIUrl, logger) | ||||
| 
 | ||||
| 	return &Controller{ | ||||
| 		Config:   *controllerConfig, | ||||
| 		opConfig: operatorConfig, | ||||
|  | @ -78,7 +79,6 @@ func (c *Controller) initController() { | |||
| 		c.logger.Fatalf("could not register ThirdPartyResource: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	c.TeamsAPIClient.RefreshTokenAction = c.getOAuthToken | ||||
| 	if infraRoles, err := c.getInfrastructureRoles(); err != nil { | ||||
| 		c.logger.Warningf("could not get infrastructure roles: %v", err) | ||||
| 	} else { | ||||
|  |  | |||
|  | @ -29,25 +29,6 @@ func (c *Controller) makeClusterConfig() cluster.Config { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *Controller) getOAuthToken() (string, error) { | ||||
| 	// Temporary getting postgresql-operator secret from the NamespaceDefault
 | ||||
| 	credentialsSecret, err := c.KubeClient. | ||||
| 		Secrets(c.opConfig.OAuthTokenSecretName.Namespace). | ||||
| 		Get(c.opConfig.OAuthTokenSecretName.Name) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		c.logger.Debugf("Oauth token secret name: %s", c.opConfig.OAuthTokenSecretName) | ||||
| 		return "", fmt.Errorf("could not get credentials secret: %v", err) | ||||
| 	} | ||||
| 	data := credentialsSecret.Data | ||||
| 
 | ||||
| 	if string(data["read-only-token-type"]) != "Bearer" { | ||||
| 		return "", fmt.Errorf("wrong token type: %v", data["read-only-token-type"]) | ||||
| 	} | ||||
| 
 | ||||
| 	return string(data["read-only-token-secret"]), nil | ||||
| } | ||||
| 
 | ||||
| func thirdPartyResource(TPRName string) *extv1beta.ThirdPartyResource { | ||||
| 	return &extv1beta.ThirdPartyResource{ | ||||
| 		ObjectMeta: v1.ObjectMeta{ | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ package spec | |||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
|  | @ -13,11 +12,10 @@ import ( | |||
| ) | ||||
| 
 | ||||
| type MaintenanceWindow struct { | ||||
| 	Everyday  bool | ||||
| 	Weekday   time.Weekday | ||||
| 	StartTime time.Time // Start time
 | ||||
| 	StartWeekday time.Weekday // Start weekday
 | ||||
| 
 | ||||
| 	EndTime   time.Time // End time
 | ||||
| 	EndWeekday time.Weekday // End weekday
 | ||||
| } | ||||
| 
 | ||||
| type Volume struct { | ||||
|  | @ -94,60 +92,48 @@ type PostgresqlList struct { | |||
| 	Items []Postgresql `json:"items"` | ||||
| } | ||||
| 
 | ||||
| var alphaRegexp = regexp.MustCompile("^[a-zA-Z]*$") | ||||
| var weekdays = map[string]int{"Sun": 0, "Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6} | ||||
| 
 | ||||
| func parseTime(s string) (t time.Time, wd time.Weekday, wdProvided bool, err error) { | ||||
| 	var timeLayout string | ||||
| 
 | ||||
| func parseTime(s string) (time.Time, error) { | ||||
| 	parts := strings.Split(s, ":") | ||||
| 	if len(parts) == 3 { | ||||
| 		if len(parts[0]) != 3 || !alphaRegexp.MatchString(parts[0]) { | ||||
| 			err = fmt.Errorf("weekday must be 3 characters length") | ||||
| 			return | ||||
| 		} | ||||
| 		timeLayout = "Mon:15:04" | ||||
| 		wdProvided = true | ||||
| 		weekday, ok := weekdays[parts[0]] | ||||
| 		if !ok { | ||||
| 			err = fmt.Errorf("incorrect weekday") | ||||
| 			return | ||||
| 		} | ||||
| 		wd = time.Weekday(weekday) | ||||
| 	} else { | ||||
| 		wdProvided = false | ||||
| 		timeLayout = "15:04" | ||||
| 	if len(parts) != 2 { | ||||
| 		return time.Time{}, fmt.Errorf("incorrect time format") | ||||
| 	} | ||||
| 	timeLayout := "15:04" | ||||
| 
 | ||||
| 	tp, err := time.Parse(timeLayout, s) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 		return time.Time{}, err | ||||
| 	} | ||||
| 	t = tp.UTC() | ||||
| 
 | ||||
| 	return | ||||
| 	return tp.UTC(), nil | ||||
| } | ||||
| 
 | ||||
| func parseWeekday(s string) (time.Weekday, error) { | ||||
| 	weekday, ok := weekdays[s] | ||||
| 	if !ok { | ||||
| 		return time.Weekday(0), fmt.Errorf("incorrect weekday") | ||||
| 	} | ||||
| 
 | ||||
| 	return time.Weekday(weekday), nil | ||||
| } | ||||
| 
 | ||||
| func (m *MaintenanceWindow) MarshalJSON() ([]byte, error) { | ||||
| 	var startWd, endWd string | ||||
| 	if m.StartWeekday == time.Monday && m.EndWeekday == time.Sunday { | ||||
| 		startWd = "" | ||||
| 		endWd = "" | ||||
| 	if m.Everyday { | ||||
| 		return []byte(fmt.Sprintf("\"%s-%s\"", | ||||
| 			m.StartTime.Format("15:04"), | ||||
| 			m.EndTime.Format("15:04"))), nil | ||||
| 	} else { | ||||
| 		startWd = m.StartWeekday.String()[:3] + ":" | ||||
| 		endWd = m.EndWeekday.String()[:3] + ":" | ||||
| 		return []byte(fmt.Sprintf("\"%s:%s-%s\"", | ||||
| 			m.Weekday.String()[:3], | ||||
| 			m.StartTime.Format("15:04"), | ||||
| 			m.EndTime.Format("15:04"))), nil | ||||
| 	} | ||||
| 
 | ||||
| 	return []byte(fmt.Sprintf("\"%s%s-%s%s\"", | ||||
| 		startWd, m.StartTime.Format("15:04"), | ||||
| 		endWd, m.EndTime.Format("15:04"))), nil | ||||
| } | ||||
| 
 | ||||
| func (m *MaintenanceWindow) UnmarshalJSON(data []byte) error { | ||||
| 	var ( | ||||
| 		got MaintenanceWindow | ||||
| 		weekdayProvidedFrom bool | ||||
| 		weekdayProvidedTo   bool | ||||
| 		err error | ||||
| 	) | ||||
| 
 | ||||
|  | @ -156,29 +142,35 @@ func (m *MaintenanceWindow) UnmarshalJSON(data []byte) error { | |||
| 		return fmt.Errorf("incorrect maintenance window format") | ||||
| 	} | ||||
| 
 | ||||
| 	got.StartTime, got.StartWeekday, weekdayProvidedFrom, err = parseTime(parts[0]) | ||||
| 	fromParts := strings.Split(parts[0], ":") | ||||
| 	switch len(fromParts) { | ||||
| 	case 3: | ||||
| 		got.Everyday = false | ||||
| 		got.Weekday, err = parseWeekday(fromParts[0]) | ||||
| 		if err != nil { | ||||
| 		return err | ||||
| 			return fmt.Errorf("could not parse weekday: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 	got.EndTime, got.EndWeekday, weekdayProvidedTo, err = parseTime(parts[1]) | ||||
| 		got.StartTime, err = parseTime(fromParts[1] + ":" + fromParts[2]) | ||||
| 	case 2: | ||||
| 		got.Everyday = true | ||||
| 		got.StartTime, err = parseTime(fromParts[0] + ":" + fromParts[1]) | ||||
| 	default: | ||||
| 		return fmt.Errorf("incorrect maintenance window format") | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return fmt.Errorf("could not parse start time: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	got.EndTime, err = parseTime(parts[1]) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not parse end time: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if got.EndTime.Before(got.StartTime) { | ||||
| 		return fmt.Errorf("'From' time must be prior to the 'To' time") | ||||
| 	} | ||||
| 
 | ||||
| 	if (int(got.StartWeekday)+6)%7 > (int(got.EndWeekday)+6)%7 { | ||||
| 		return fmt.Errorf("'From' weekday must be prior to the 'To' weekday") | ||||
| 	} | ||||
| 
 | ||||
| 	if !weekdayProvidedFrom || !weekdayProvidedTo { | ||||
| 		got.StartWeekday = time.Monday | ||||
| 		got.EndWeekday = time.Sunday | ||||
| 	} | ||||
| 
 | ||||
| 	*m = got | ||||
| 
 | ||||
| 	return nil | ||||
|  | @ -202,13 +194,12 @@ func (pl *PostgresqlList) GetListMeta() unversioned.List { | |||
| 
 | ||||
| func extractClusterName(clusterName string, teamName string) (string, error) { | ||||
| 	teamNameLen := len(teamName) | ||||
| 
 | ||||
| 	if len(clusterName) < teamNameLen+2 { | ||||
| 		return "", fmt.Errorf("name is too short") | ||||
| 	} | ||||
| 
 | ||||
| 	if teamNameLen == 0 { | ||||
| 		return "", fmt.Errorf("Team name is empty") | ||||
| 		return "", fmt.Errorf("team name is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	if strings.ToLower(clusterName[:teamNameLen+1]) != strings.ToLower(teamName)+"-" { | ||||
|  |  | |||
|  | @ -41,31 +41,19 @@ type API struct { | |||
| 	url        string | ||||
| 	httpClient *http.Client | ||||
| 	logger     *logrus.Entry | ||||
| 	RefreshTokenAction func() (string, error) | ||||
| 	enabled            bool | ||||
| } | ||||
| 
 | ||||
| func NewTeamsAPI(url string, log *logrus.Logger, enabled bool) *API { | ||||
| func NewTeamsAPI(url string, log *logrus.Logger) *API { | ||||
| 	t := API{ | ||||
| 		url:        strings.TrimRight(url, "/"), | ||||
| 		httpClient: &http.Client{}, | ||||
| 		logger:     log.WithField("pkg", "teamsapi"), | ||||
| 		enabled:    enabled, | ||||
| 	} | ||||
| 
 | ||||
| 	return &t | ||||
| } | ||||
| 
 | ||||
| func (t *API) TeamInfo(teamID string) (*Team, error) { | ||||
| 	// TODO: avoid getting a new token on every call to the Teams API.
 | ||||
| 	if !t.enabled { | ||||
| 		t.logger.Debug("Team API is disabled, returning empty list of members") | ||||
| 		return &Team{}, nil | ||||
| 	} | ||||
| 	token, err := t.RefreshTokenAction() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| func (t *API) TeamInfo(teamID, token string) (*Team, error) { | ||||
| 	url := fmt.Sprintf("%s/teams/%s", t.url, teamID) | ||||
| 	t.logger.Debugf("Request url: %s", url) | ||||
| 	req, err := http.NewRequest("GET", url, nil) | ||||
|  | @ -84,7 +72,7 @@ func (t *API) TeamInfo(teamID string) (*Team, error) { | |||
| 		d := json.NewDecoder(resp.Body) | ||||
| 		err = d.Decode(&raw) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, fmt.Errorf("team API query failed with status code %d and malformed response: %v", resp.StatusCode, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if errMessage, ok := raw["error"]; ok { | ||||
|  | @ -97,7 +85,7 @@ func (t *API) TeamInfo(teamID string) (*Team, error) { | |||
| 	d := json.NewDecoder(resp.Body) | ||||
| 	err = d.Decode(teamInfo) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, fmt.Errorf("could not parse team API response: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return teamInfo, nil | ||||
|  |  | |||
|  | @ -0,0 +1,182 @@ | |||
| package teams | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logger = logrus.New() | ||||
| 	token  = "ec45b1cfbe7100c6315d183a3eb6cec0M2U1LWJkMzEtZDgzNzNmZGQyNGM3IiwiYXV0aF90aW1lIjoxNDkzNzMwNzQ1LCJpc3MiOiJodHRwcz" | ||||
| ) | ||||
| 
 | ||||
| var teamsAPItc = []struct { | ||||
| 	in     string | ||||
| 	inCode int | ||||
| 	out    *Team | ||||
| 	err    error | ||||
| }{ | ||||
| 	{`{ | ||||
| "dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", | ||||
| "id": "acid", | ||||
| "id_name": "ACID", | ||||
| "team_id": "111222", | ||||
| "type": "official", | ||||
| "name": "Acid team name", | ||||
| "mail": [ | ||||
| "email1@example.com", | ||||
| "email2@example.com" | ||||
| ], | ||||
| "alias": [ | ||||
| "acid" | ||||
| ], | ||||
| "member": [ | ||||
|   "member1", | ||||
|   "member2", | ||||
|   "member3" | ||||
| ], | ||||
| "infrastructure-accounts": [ | ||||
| { | ||||
|   "id": "1234512345", | ||||
|   "name": "acid", | ||||
|   "provider": "aws", | ||||
|   "type": "aws", | ||||
|   "description": "", | ||||
|   "owner": "acid", | ||||
|   "owner_dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", | ||||
|   "disabled": false | ||||
| }, | ||||
| { | ||||
|   "id": "5432154321", | ||||
|   "name": "db", | ||||
|   "provider": "aws", | ||||
|   "type": "aws", | ||||
|   "description": "", | ||||
|   "owner": "acid", | ||||
|   "owner_dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", | ||||
|   "disabled": false | ||||
| } | ||||
| ], | ||||
| "cost_center": "00099999", | ||||
| "delivery_lead": "member4", | ||||
| "parent_team_id": "111221" | ||||
| }`, | ||||
| 		200, | ||||
| 		&Team{ | ||||
| 			Dn:           "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", | ||||
| 			ID:           "acid", | ||||
| 			TeamName:     "ACID", | ||||
| 			TeamID:       "111222", | ||||
| 			Type:         "official", | ||||
| 			FullName:     "Acid team name", | ||||
| 			Aliases:      []string{"acid"}, | ||||
| 			Mails:        []string{"email1@example.com", "email2@example.com"}, | ||||
| 			Members:      []string{"member1", "member2", "member3"}, | ||||
| 			CostCenter:   "00099999", | ||||
| 			DeliveryLead: "member4", | ||||
| 			ParentTeamID: "111221", | ||||
| 			InfrastructureAccounts: []InfrastructureAccount{ | ||||
| 				{ | ||||
| 					ID:          "1234512345", | ||||
| 					Name:        "acid", | ||||
| 					Provider:    "aws", | ||||
| 					Type:        "aws", | ||||
| 					Description: "", | ||||
| 					Owner:       "acid", | ||||
| 					OwnerDn:     "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", | ||||
| 					Disabled:    false}, | ||||
| 				{ | ||||
| 					ID:          "5432154321", | ||||
| 					Name:        "db", | ||||
| 					Provider:    "aws", | ||||
| 					Type:        "aws", | ||||
| 					Description: "", | ||||
| 					Owner:       "acid", | ||||
| 					OwnerDn:     "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", | ||||
| 					Disabled:    false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		nil}, { | ||||
| 		`{"error": "Access Token not valid"}`, | ||||
| 		401, | ||||
| 		nil, | ||||
| 		fmt.Errorf(`team API query failed with status code 401 and message: '"Access Token not valid"'`), | ||||
| 	}, | ||||
| 	{ | ||||
| 		`{"status": "I'm a teapot'"}`, | ||||
| 		418, | ||||
| 		nil, | ||||
| 		fmt.Errorf(`team API query failed with status code 418`), | ||||
| 	}, | ||||
| 	{ | ||||
| 		`{"status": "I'm a teapot`, | ||||
| 		418, | ||||
| 		nil, | ||||
| 		fmt.Errorf(`team API query failed with status code 418 and malformed response: unexpected EOF`), | ||||
| 	}, | ||||
| 	{ | ||||
| 		`{"status": "I'm a teapot`, | ||||
| 		200, | ||||
| 		nil, | ||||
| 		fmt.Errorf(`could not parse team API response: unexpected EOF`), | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| var requestsURLtc = []struct { | ||||
| 	url string | ||||
| 	err error | ||||
| }{ | ||||
| 	{ | ||||
| 		"coffee://localhost/", | ||||
| 		fmt.Errorf(`Get coffee://localhost/teams/acid: unsupported protocol scheme "coffee"`), | ||||
| 	}, | ||||
| 	{ | ||||
| 		"http://192.168.0.%31/", | ||||
| 		fmt.Errorf(`parse http://192.168.0.%%31/teams/acid: invalid URL escape "%%31"`), | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| func TestInfo(t *testing.T) { | ||||
| 	for _, tc := range teamsAPItc { | ||||
| 		func() { | ||||
| 			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 				if r.Header.Get("Authorization") != "Bearer " + token { | ||||
| 					t.Errorf("Authorization token is wrong or not provided") | ||||
| 				} | ||||
| 				w.WriteHeader(tc.inCode) | ||||
| 				fmt.Fprint(w, tc.in) | ||||
| 			})) | ||||
| 			defer ts.Close() | ||||
| 			api := NewTeamsAPI(ts.URL, logger) | ||||
| 
 | ||||
| 			actual, err := api.TeamInfo("acid", token) | ||||
| 			if err != nil && err.Error() != tc.err.Error() { | ||||
| 				t.Errorf("Expected error: %v, got: %v", tc.err, err) | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			if !reflect.DeepEqual(actual, tc.out) { | ||||
| 				t.Errorf("Expected %#v, got: %#v", tc.out, actual) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestRequest(t *testing.T) { | ||||
| 	for _, tc := range requestsURLtc { | ||||
| 		api := NewTeamsAPI(tc.url, logger) | ||||
| 		resp, err := api.TeamInfo("acid", token) | ||||
| 		if resp != nil { | ||||
| 			t.Errorf("Response expected to be nil") | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if err.Error() != tc.err.Error() { | ||||
| 			t.Errorf("Expected error: %v, got: %v", tc.err, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -41,7 +41,7 @@ func NameFromMeta(meta v1.ObjectMeta) spec.NamespacedName { | |||
| } | ||||
| 
 | ||||
| func PGUserPassword(user spec.PgUser) string { | ||||
| 	if (len(user.Password) == md5.Size && user.Password[:3] == md5prefix) || user.Password == "" { | ||||
| 	if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) || user.Password == "" { | ||||
| 		// Avoid processing already encrypted or empty passwords
 | ||||
| 		return user.Password | ||||
| 	} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue