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 {
|
||||
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)+"-" {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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