Merge branch 'master' into feature/tests

# Conflicts:
#	pkg/spec/postgresql.go
This commit is contained in:
Murat Kabilov 2017-06-06 14:56:59 +02:00
commit 9086beaa40
8 changed files with 273 additions and 101 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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 {

View File

@ -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{

View File

@ -3,7 +3,6 @@ package spec
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
@ -13,11 +12,10 @@ import (
)
type MaintenanceWindow struct {
StartTime time.Time // Start time
StartWeekday time.Weekday // Start weekday
EndTime time.Time // End time
EndWeekday time.Weekday // End weekday
Everyday bool
Weekday time.Weekday
StartTime time.Time // Start time
EndTime time.Time // End time
}
type Volume struct {
@ -94,61 +92,49 @@ 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
got MaintenanceWindow
err error
)
parts := strings.Split(string(data[1:len(data)-1]), "-")
@ -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 fmt.Errorf("could not parse weekday: %v", err)
}
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, got.EndWeekday, weekdayProvidedTo, err = parseTime(parts[1])
got.EndTime, err = parseTime(parts[1])
if err != nil {
return err
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)+"-" {

View File

@ -38,34 +38,22 @@ type Team struct {
}
type API struct {
url string
httpClient *http.Client
logger *logrus.Entry
RefreshTokenAction func() (string, error)
enabled bool
url string
httpClient *http.Client
logger *logrus.Entry
}
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

View File

@ -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)
}
}
}

View File

@ -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
}