Skip to content
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

Allow peribolos to manage repo permissions for teams #12144

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions prow/cmd/peribolos/BUILD.bazel
Expand Up @@ -13,6 +13,7 @@ go_library(
"//prow/config:go_default_library",
"//prow/config/org:go_default_library",
"//prow/config/secret:go_default_library",
"//prow/errorutil:go_default_library",
"//prow/flagutil:go_default_library",
"//prow/github:go_default_library",
"//prow/logrusutil:go_default_library",
Expand Down
4 changes: 4 additions & 0 deletions prow/cmd/peribolos/README.md
Expand Up @@ -46,6 +46,9 @@ orgs:
- anne
maintainers:
- jane
repos: # Ensure the team has the following permissions levels on repos in the org
some-repo: admin
Copy link
Contributor

Choose a reason for hiding this comment

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

Once we have repo management I feel like this would be better to scope under the repo.

There's no guarantee that a some-repo exists (although I guess one could also argue that there's no guarantee that some-team exists)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about this, too, but:

  1. GitHub's API works through the team, so I figured it's best if we mirror it for code complexity
  2. Running on repo, not on team, removes the nice property for dumps above and will make honoring the flag to ignore teams harder
  3. Running per repo also adds a substantial API burden to list repo state through teams

other-repo: read
another-team:
...
...
Expand All @@ -66,6 +69,7 @@ This config will:
- Rename the backend team to node
- Add anne as a member and jane as a maintainer to node
- Similar things for another-team (details elided)
* Ensure that the team has admin rights to `some-repo`, read access to `other-repo` and no other privileges

Note that any fields missing from the config will not be managed by peribolos. So if description is missing from the org setting, the current value will remain.

Expand Down
107 changes: 106 additions & 1 deletion prow/cmd/peribolos/main.go
Expand Up @@ -30,6 +30,7 @@ import (
"k8s.io/test-infra/prow/config"
"k8s.io/test-infra/prow/config/org"
"k8s.io/test-infra/prow/config/secret"
"k8s.io/test-infra/prow/errorutil"
"k8s.io/test-infra/prow/flagutil"
"k8s.io/test-infra/prow/github"
"k8s.io/test-infra/prow/logrusutil"
Expand All @@ -55,10 +56,12 @@ type options struct {
fixOrgMembers bool
fixTeamMembers bool
fixTeams bool
fixTeamRepos bool
ignoreSecretTeams bool
github flagutil.GitHubOptions
tokenBurst int
tokensPerHour int
logLevel string
}

func parseOptions() options {
Expand Down Expand Up @@ -86,6 +89,8 @@ func (o *options) parseArgs(flags *flag.FlagSet, args []string) error {
flags.BoolVar(&o.fixOrgMembers, "fix-org-members", false, "Add/remove org members if set")
flags.BoolVar(&o.fixTeams, "fix-teams", false, "Create/delete/update teams if set")
flags.BoolVar(&o.fixTeamMembers, "fix-team-members", false, "Add/remove team members if set")
flags.BoolVar(&o.fixTeamRepos, "fix-team-repos", false, "Add/remove team permissions on repos if set")
flags.StringVar(&o.logLevel, "log-level", logrus.InfoLevel.String(), fmt.Sprintf("Logging level, one of %v", logrus.AllLevels))
o.github.AddFlags(flags)
if err := flags.Parse(args); err != nil {
return err
Expand Down Expand Up @@ -118,6 +123,16 @@ func (o *options) parseArgs(flags *flag.FlagSet, args []string) error {
return fmt.Errorf("--fix-team-members requires --fix-teams")
}

if o.fixTeamRepos && !o.fixTeams {
return fmt.Errorf("--fix-team-repos requires --fix-teams")
}

level, err := logrus.ParseLevel(o.logLevel)
if err != nil {
return fmt.Errorf("--log-level invalid: %v", err)
}
logrus.SetLevel(level)

return nil
}

Expand Down Expand Up @@ -171,6 +186,7 @@ type dumpClient interface {
ListOrgMembers(org, role string) ([]github.TeamMember, error)
ListTeams(org string) ([]github.Team, error)
ListTeamMembers(id int, role string) ([]github.TeamMember, error)
ListTeamRepos(id int) ([]github.Repo, error)
}

func dumpOrgConfig(client dumpClient, orgName string, ignoreSecretTeams bool) (*org.Config, error) {
Expand All @@ -187,39 +203,46 @@ func dumpOrgConfig(client dumpClient, orgName string, ignoreSecretTeams bool) (*
out.Metadata.Location = &meta.Location
out.Metadata.HasOrganizationProjects = &meta.HasOrganizationProjects
out.Metadata.HasRepositoryProjects = &meta.HasRepositoryProjects
drp := org.RepoPermissionLevel(meta.DefaultRepositoryPermission)
drp := github.RepoPermissionLevel(meta.DefaultRepositoryPermission)
out.Metadata.DefaultRepositoryPermission = &drp
out.Metadata.MembersCanCreateRepositories = &meta.MembersCanCreateRepositories

admins, err := client.ListOrgMembers(orgName, github.RoleAdmin)
if err != nil {
return nil, fmt.Errorf("failed to list org admins: %v", err)
}
logrus.Debugf("Found %d admins", len(admins))
for _, m := range admins {
logrus.WithField("login", m.Login).Debug("Recording admin.")
out.Admins = append(out.Admins, m.Login)
}

orgMembers, err := client.ListOrgMembers(orgName, github.RoleMember)
if err != nil {
return nil, fmt.Errorf("failed to list org members: %v", err)
}
logrus.Debugf("Found %d members", len(orgMembers))
for _, m := range orgMembers {
logrus.WithField("login", m.Login).Debug("Recording member.")
out.Members = append(out.Members, m.Login)
}

teams, err := client.ListTeams(orgName)
if err != nil {
return nil, fmt.Errorf("failed to list teams: %v", err)
}
logrus.Debugf("Found %d teams", len(teams))

names := map[int]string{} // what's the name of a team?
idMap := map[int]org.Team{} // metadata for a team
children := map[int][]int{} // what children does it have
var tops []int // what are the top-level teams

for _, t := range teams {
logger := logrus.WithFields(logrus.Fields{"id": t.ID, "name": t.Name})
p := org.Privacy(t.Privacy)
if ignoreSecretTeams && p == org.Secret {
logger.Debug("Ignoring secret team.")
continue
}
d := t.Description
Expand All @@ -231,30 +254,48 @@ func dumpOrgConfig(client dumpClient, orgName string, ignoreSecretTeams bool) (*
Maintainers: []string{},
Members: []string{},
Children: map[string]org.Team{},
Repos: map[string]github.RepoPermissionLevel{},
}
maintainers, err := client.ListTeamMembers(t.ID, github.RoleMaintainer)
if err != nil {
return nil, fmt.Errorf("failed to list team %d(%s) maintainers: %v", t.ID, t.Name, err)
}
logger.Debugf("Found %d maintainers.", len(maintainers))
for _, m := range maintainers {
logger.WithField("login", m.Login).Debug("Recording maintainer.")
nt.Maintainers = append(nt.Maintainers, m.Login)
}
teamMembers, err := client.ListTeamMembers(t.ID, github.RoleMember)
if err != nil {
return nil, fmt.Errorf("failed to list team %d(%s) members: %v", t.ID, t.Name, err)
}
logger.Debugf("Found %d members.", len(teamMembers))
for _, m := range teamMembers {
logger.WithField("login", m.Login).Debug("Recording member.")
nt.Members = append(nt.Members, m.Login)
}

names[t.ID] = t.Name
idMap[t.ID] = nt

if t.Parent == nil { // top level team
logger.Debug("Marking as top-level team.")
tops = append(tops, t.ID)
} else { // add this id to the list of the parent's children
logger.Debugf("Marking as child team of %d.", t.Parent.ID)
children[t.Parent.ID] = append(children[t.Parent.ID], t.ID)
}

repos, err := client.ListTeamRepos(t.ID)
if err != nil {
return nil, fmt.Errorf("failed to list team %d(%s) repos: %v", t.ID, t.Name, err)
}
logger.Debugf("Found %d repo permissions.", len(repos))
for _, repo := range repos {
level := github.LevelFromPermissions(repo.Permissions)
logger.WithFields(logrus.Fields{"repo": repo, "permission": level}).Debug("Recording repo permission.")
nt.Repos[repo.Name] = level
}
}

var makeChild func(id int) org.Team
Expand Down Expand Up @@ -726,6 +767,14 @@ func configureOrg(opt options, client *github.Client, orgName string, orgConfig
if err != nil {
return fmt.Errorf("failed to configure %s teams: %v", orgName, err)
}

if !opt.fixTeamRepos {
logrus.Infof("Skipping team repo permissions configuration")
continue
}
if err := configureTeamRepos(client, githubTeams, name, orgName, team); err != nil {
return fmt.Errorf("failed to configure %s team %s repos: %v", orgName, name, err)
}
}
return nil
}
Expand Down Expand Up @@ -811,6 +860,62 @@ func configureTeam(client editTeamClient, orgName, teamName string, team org.Tea
return nil
}

type teamRepoClient interface {
ListTeamRepos(id int) ([]github.Repo, error)
UpdateTeamRepo(id int, org, repo string, permission github.RepoPermissionLevel) error
RemoveTeamRepo(id int, org, repo string) error
}

// configureTeamRepos updates the list of repos that the team has permissions for when necessary
func configureTeamRepos(client teamRepoClient, githubTeams map[string]github.Team, name, orgName string, team org.Team) error {
gt, ok := githubTeams[name]
if !ok { // configureTeams is buggy if this is the case
return fmt.Errorf("%s not found in id list", name)
}

want := team.Repos
have := map[string]github.RepoPermissionLevel{}
repos, err := client.ListTeamRepos(gt.ID)
if err != nil {
return fmt.Errorf("failed to list team %d(%s) repos: %v", gt.ID, name, err)
}
for _, repo := range repos {
have[repo.Name] = github.LevelFromPermissions(repo.Permissions)
}

actions := map[string]github.RepoPermissionLevel{}
for wantRepo, wantPermission := range want {
if havePermission, haveRepo := have[wantRepo]; haveRepo && havePermission == wantPermission {
// nothing to do
continue
}
// create or update this permission
actions[wantRepo] = wantPermission
}

for haveRepo := range have {
if _, wantRepo := want[haveRepo]; !wantRepo {
// should remove these permissions
actions[haveRepo] = github.None
}
}

var updateErrors []error
for repo, permission := range actions {
var err error
if permission == github.None {
err = client.RemoveTeamRepo(gt.ID, orgName, repo)
} else {
err = client.UpdateTeamRepo(gt.ID, orgName, repo, permission)
}
if err != nil {
updateErrors = append(updateErrors, fmt.Errorf("failed to update team %d(%s) permissions on repo %s to %s: %v", gt.ID, name, repo, permission, err))
}
}

return errorutil.NewAggregate(updateErrors...)
}

// teamMembersClient can list/remove/update people to a team.
type teamMembersClient interface {
ListTeamMembers(id int, role string) ([]github.TeamMember, error)
Expand Down