PostgresTeam CRD for advanced team management (#1165)

* PostgresTeamCRD for advanced team management

* rework internal structure to be closer to CRD

* superusers instead of admin

* add more util functions and unit tests

* fix initHumanUsers

* check for superusers when creating normal teams

* polishing and fixes

* adding the essential missing pieces

* add documentation and update rbac

* reflect some feedback

* reflect more feedback

* fixing debug logs and raise QueueResyncPeriodTPR

* add two more flags to disable CRD and its superuser support

* fix chart

* update go modules

* move to client 1.19.3 and update codegen
This commit is contained in:
Felix Kunde
2020-10-28 10:40:10 +01:00
committed by GitHub
parent 3a86dfc8bb
commit d658b9672e
39 changed files with 1509 additions and 47 deletions
+118
View File
@@ -0,0 +1,118 @@
package teams
import (
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"github.com/zalando/postgres-operator/pkg/util"
)
// PostgresTeamMap is the operator's internal representation of all PostgresTeam CRDs
type PostgresTeamMap map[string]postgresTeamMembership
type postgresTeamMembership struct {
AdditionalSuperuserTeams []string
AdditionalTeams []string
AdditionalMembers []string
}
type teamHashSet map[string]map[string]struct{}
func (ths *teamHashSet) has(team string) bool {
_, ok := (*ths)[team]
return ok
}
func (ths *teamHashSet) add(newTeam string, newSet []string) {
set := make(map[string]struct{})
if ths.has(newTeam) {
set = (*ths)[newTeam]
}
for _, t := range newSet {
set[t] = struct{}{}
}
(*ths)[newTeam] = set
}
func (ths *teamHashSet) toMap() map[string][]string {
newTeamMap := make(map[string][]string)
for team, items := range *ths {
list := []string{}
for item := range items {
list = append(list, item)
}
newTeamMap[team] = list
}
return newTeamMap
}
func (ths *teamHashSet) mergeCrdMap(crdTeamMap map[string][]string) {
for t, at := range crdTeamMap {
ths.add(t, at)
}
}
func fetchTeams(teamset *map[string]struct{}, set teamHashSet) {
for key := range set {
(*teamset)[key] = struct{}{}
}
}
func (ptm *PostgresTeamMap) fetchAdditionalTeams(team string, superuserTeams bool, transitive bool, exclude []string) []string {
var teams []string
if superuserTeams {
teams = (*ptm)[team].AdditionalSuperuserTeams
} else {
teams = (*ptm)[team].AdditionalTeams
}
if transitive {
exclude = append(exclude, team)
for _, additionalTeam := range teams {
if !(util.SliceContains(exclude, additionalTeam)) {
transitiveTeams := (*ptm).fetchAdditionalTeams(additionalTeam, superuserTeams, transitive, exclude)
for _, transitiveTeam := range transitiveTeams {
if !(util.SliceContains(exclude, transitiveTeam)) && !(util.SliceContains(teams, transitiveTeam)) {
teams = append(teams, transitiveTeam)
}
}
}
}
}
return teams
}
// GetAdditionalTeams function to retrieve list of additional teams
func (ptm *PostgresTeamMap) GetAdditionalTeams(team string, transitive bool) []string {
return ptm.fetchAdditionalTeams(team, false, transitive, []string{})
}
// GetAdditionalSuperuserTeams function to retrieve list of additional superuser teams
func (ptm *PostgresTeamMap) GetAdditionalSuperuserTeams(team string, transitive bool) []string {
return ptm.fetchAdditionalTeams(team, true, transitive, []string{})
}
// Load function to import data from PostgresTeam CRD
func (ptm *PostgresTeamMap) Load(pgTeams *acidv1.PostgresTeamList) {
superuserTeamSet := teamHashSet{}
teamSet := teamHashSet{}
teamMemberSet := teamHashSet{}
teamIDs := make(map[string]struct{})
for _, pgTeam := range pgTeams.Items {
superuserTeamSet.mergeCrdMap(pgTeam.Spec.AdditionalSuperuserTeams)
teamSet.mergeCrdMap(pgTeam.Spec.AdditionalTeams)
teamMemberSet.mergeCrdMap(pgTeam.Spec.AdditionalMembers)
}
fetchTeams(&teamIDs, superuserTeamSet)
fetchTeams(&teamIDs, teamSet)
fetchTeams(&teamIDs, teamMemberSet)
for teamID := range teamIDs {
(*ptm)[teamID] = postgresTeamMembership{
AdditionalSuperuserTeams: util.CoalesceStrArr(superuserTeamSet.toMap()[teamID], []string{}),
AdditionalTeams: util.CoalesceStrArr(teamSet.toMap()[teamID], []string{}),
AdditionalMembers: util.CoalesceStrArr(teamMemberSet.toMap()[teamID], []string{}),
}
}
}
+194
View File
@@ -0,0 +1,194 @@
package teams
import (
"testing"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"github.com/zalando/postgres-operator/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
True = true
False = false
pgTeamList = acidv1.PostgresTeamList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "v1",
},
Items: []acidv1.PostgresTeam{
{
TypeMeta: metav1.TypeMeta{
Kind: "PostgresTeam",
APIVersion: "acid.zalan.do/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "teamAB",
},
Spec: acidv1.PostgresTeamSpec{
AdditionalSuperuserTeams: map[string][]string{"teamA": []string{"teamB", "team24x7"}, "teamB": []string{"teamA", "teamC", "team24x7"}},
AdditionalTeams: map[string][]string{"teamA": []string{"teamC"}, "teamB": []string{}},
AdditionalMembers: map[string][]string{"team24x7": []string{"optimusprime"}, "teamB": []string{"drno"}},
},
}, {
TypeMeta: metav1.TypeMeta{
Kind: "PostgresTeam",
APIVersion: "acid.zalan.do/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "teamC",
},
Spec: acidv1.PostgresTeamSpec{
AdditionalSuperuserTeams: map[string][]string{"teamC": []string{"team24x7"}},
AdditionalTeams: map[string][]string{"teamA": []string{"teamC"}, "teamC": []string{"teamA", "teamB", "acid"}},
AdditionalMembers: map[string][]string{"acid": []string{"batman"}},
},
},
},
}
)
// PostgresTeamMap is the operator's internal representation of all PostgresTeam CRDs
func TestLoadingPostgresTeamCRD(t *testing.T) {
tests := []struct {
name string
crd acidv1.PostgresTeamList
ptm PostgresTeamMap
error string
}{
{
"Check that CRD is imported correctly into the internal format",
pgTeamList,
PostgresTeamMap{
"teamA": {
AdditionalSuperuserTeams: []string{"teamB", "team24x7"},
AdditionalTeams: []string{"teamC"},
AdditionalMembers: []string{},
},
"teamB": {
AdditionalSuperuserTeams: []string{"teamA", "teamC", "team24x7"},
AdditionalTeams: []string{},
AdditionalMembers: []string{"drno"},
},
"teamC": {
AdditionalSuperuserTeams: []string{"team24x7"},
AdditionalTeams: []string{"teamA", "teamB", "acid"},
AdditionalMembers: []string{},
},
"team24x7": {
AdditionalSuperuserTeams: []string{},
AdditionalTeams: []string{},
AdditionalMembers: []string{"optimusprime"},
},
"acid": {
AdditionalSuperuserTeams: []string{},
AdditionalTeams: []string{},
AdditionalMembers: []string{"batman"},
},
},
"Mismatch between PostgresTeam CRD and internal map",
},
}
for _, tt := range tests {
postgresTeamMap := PostgresTeamMap{}
postgresTeamMap.Load(&tt.crd)
for team, ptmeamMembership := range postgresTeamMap {
if !util.IsEqualIgnoreOrder(ptmeamMembership.AdditionalSuperuserTeams, tt.ptm[team].AdditionalSuperuserTeams) {
t.Errorf("%s: %v: expected additional members %#v, got %#v", tt.name, tt.error, tt.ptm, postgresTeamMap)
}
if !util.IsEqualIgnoreOrder(ptmeamMembership.AdditionalTeams, tt.ptm[team].AdditionalTeams) {
t.Errorf("%s: %v: expected additional teams %#v, got %#v", tt.name, tt.error, tt.ptm, postgresTeamMap)
}
if !util.IsEqualIgnoreOrder(ptmeamMembership.AdditionalMembers, tt.ptm[team].AdditionalMembers) {
t.Errorf("%s: %v: expected additional superuser teams %#v, got %#v", tt.name, tt.error, tt.ptm, postgresTeamMap)
}
}
}
}
// TestGetAdditionalTeams if returns teams with and without transitive dependencies
func TestGetAdditionalTeams(t *testing.T) {
tests := []struct {
name string
team string
transitive bool
teams []string
error string
}{
{
"Check that additional teams are returned",
"teamA",
false,
[]string{"teamC"},
"GetAdditionalTeams returns wrong list",
},
{
"Check that additional teams are returned incl. transitive teams",
"teamA",
true,
[]string{"teamC", "teamB", "acid"},
"GetAdditionalTeams returns wrong list",
},
{
"Check that empty list is returned",
"teamB",
false,
[]string{},
"GetAdditionalTeams returns wrong list",
},
}
postgresTeamMap := PostgresTeamMap{}
postgresTeamMap.Load(&pgTeamList)
for _, tt := range tests {
additionalTeams := postgresTeamMap.GetAdditionalTeams(tt.team, tt.transitive)
if !util.IsEqualIgnoreOrder(additionalTeams, tt.teams) {
t.Errorf("%s: %v: expected additional teams %#v, got %#v", tt.name, tt.error, tt.teams, additionalTeams)
}
}
}
// TestGetAdditionalSuperuserTeams if returns teams with and without transitive dependencies
func TestGetAdditionalSuperuserTeams(t *testing.T) {
tests := []struct {
name string
team string
transitive bool
teams []string
error string
}{
{
"Check that additional superuser teams are returned",
"teamA",
false,
[]string{"teamB", "team24x7"},
"GetAdditionalSuperuserTeams returns wrong list",
},
{
"Check that additional superuser teams are returned incl. transitive superuser teams",
"teamA",
true,
[]string{"teamB", "teamC", "team24x7"},
"GetAdditionalSuperuserTeams returns wrong list",
},
{
"Check that empty list is returned",
"team24x7",
false,
[]string{},
"GetAdditionalSuperuserTeams returns wrong list",
},
}
postgresTeamMap := PostgresTeamMap{}
postgresTeamMap.Load(&pgTeamList)
for _, tt := range tests {
additionalTeams := postgresTeamMap.GetAdditionalSuperuserTeams(tt.team, tt.transitive)
if !util.IsEqualIgnoreOrder(additionalTeams, tt.teams) {
t.Errorf("%s: %v: expected additional teams %#v, got %#v", tt.name, tt.error, tt.teams, additionalTeams)
}
}
}