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

Add remove-members helper tool implemented in go #2575

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions cmd/remove-members/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package main

import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

flag "github.com/spf13/pflag"
"k8s.io/test-infra/prow/config/org"
"sigs.k8s.io/yaml"
)

type options struct {
confirm bool
configPath string
}

func parseOptions() options {
var o options
flag.StringVar(&o.configPath, "path", ".", "Path to config directory/subdirectory")
flag.BoolVar(&o.confirm, "confirm", false, "Modify the actual files or just simulate changes")

flag.Parse()

flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: remove-members [--confirm] [--path] member-file (file-containing-members-list)\n")
flag.PrintDefaults()
}

if len(flag.Args()) != 1 {
flag.Usage()
os.Exit(1)
}

return o
}

func main() {

o := parseOptions()

memberList, err := readMemberList(flag.Args()[0])
if err != nil {
log.Fatal(err)
}

if err = removeMembers(memberList, o); err != nil {
log.Fatal(err)
}

}

//readMemberList reads the list of members to be removed from the given filepath
func readMemberList(path string) ([]string, error) {
file, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}

var members = strings.Split(string(file), "\n")
return members, nil
}

//removeMembers walks through the config directory and removes the occurences of the given member name
func removeMembers(memberList []string, o options) error {
for _, member := range memberList {
var orgs, teams []string
Copy link

@shamsher31 shamsher31 Jul 9, 2021

Choose a reason for hiding this comment

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

This will redeclare orgs and teams with each iteration, better to move it outside of for loop.

fmt.Print(member)

if err := filepath.Walk(o.configPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}

if filepath.Ext(path) != ".yaml" {
return nil
}
removed, removals, err := removeMemberFromFile(member, path, info, o.confirm)
if err != nil {
return err
}

//Record the org/team name when a member is removed from it
if removed {
orgs = append(orgs, removals["orgs"]...)
teams = append(teams, removals["teams"]...)
}

return nil
}); err != nil {
return err
}

if len(orgs) > 0 {
fmt.Printf("\n Found %s in %s org(s)", member, strings.Join(orgs, ", "))
} else {
fmt.Printf("\n Found %s in no org", member)
}

if len(teams) > 0 {
fmt.Printf("\n Found %s in %s team(s)", member, strings.Join(teams, ", "))
} else {
fmt.Printf("\n Found %s in no team", member)
}

fmt.Printf("\n Total number of occurences: %d\n", len(orgs)+len(teams))

//Proceed to committing changes if member is actually removed from somewhere
if len(orgs)+len(teams) > 0 {
commitRemovedMembers(member, orgs, teams, o.confirm)
}
}

return nil
}

func removeMemberFromFile(member string, path string, info os.FileInfo, confirm bool) (bool, map[string][]string, error) {

content, err := ioutil.ReadFile(path)
if err != nil {
return false, nil, err
}

var cfg org.Config
if err := yaml.Unmarshal(content, &cfg); err != nil {
return false, nil, err
}

removals := map[string][]string{
"orgs": fetchOrgRemovals(cfg, []string{}, member, path),
"teams": fetchTeamRemovals(cfg.Teams, []string{}, member),
}

if len(removals["orgs"])+len(removals["teams"]) > 0 {
re := regexp.MustCompile(`(\s+)?- ` + member + `(.*)?`)

matches := re.FindAllIndex(content, -1)

//Making Sure count from parsed config and regex matches are same
Copy link

@shamsher31 shamsher31 Jul 9, 2021

Choose a reason for hiding this comment

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

Suggested change
//Making Sure count from parsed config and regex matches are same
// Making sure count from parsed config and regex matches are the same

if len(matches) != len(removals["orgs"])+len(removals["teams"]) {
log.Printf("\n\n Mismatch in regex count and removal count at %s\n", path)
}

if confirm {
updatedContent := re.ReplaceAll(content, []byte(""))
if err = ioutil.WriteFile(path, updatedContent, info.Mode()); err != nil {
return false, removals, err
}

cmd := exec.Command("git", "add", path)
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}

return true, removals, nil
}

return false, removals, nil

}

func commitRemovedMembers(member string, orgs []string, teams []string, confirm bool) {
cmd := []string{"echo", "git", "commit"}
if confirm {
cmd = cmd[1:]
}

orgCommitMsg := "Remove " + member + " from the "
if len(orgs) == 1 {
orgCommitMsg += orgs[0] + " org"
cmd = append(cmd, "-m", orgCommitMsg)
} else if len(orgs) >= 1 {
orgCommitMsg += strings.Join(orgs, ", ") + " orgs"
cmd = append(cmd, "-m", orgCommitMsg)
}

teamCommitMsg := "Remove " + member + " from "
if len(teams) == 1 {
teamCommitMsg += teams[0] + " team"
cmd = append(cmd, "-m", teamCommitMsg)
} else if len(teams) >= 1 {
teamCommitMsg += strings.Join(teams, ", ") + " teams"
cmd = append(cmd, "-m", teamCommitMsg)
}

e := exec.Command(cmd[0], cmd[1:]...)
if err := e.Run(); err != nil {
log.Fatal(err)
}

}

func fetchOrgRemovals(cfg org.Config, removals []string, member string, path string) []string {
if cfg.Name != nil {
removals = fetchRemovals(cfg.Members, removals, member, *cfg.Name)
removals = fetchRemovals(cfg.Admins, removals, member, *cfg.Name)
}
return removals
}

func fetchTeamRemovals(teams map[string]org.Team, removals []string, member string) []string {
for teamName, v := range teams {
removals = fetchRemovals(v.Members, removals, member, teamName)
removals = fetchRemovals(v.Maintainers, removals, member, teamName)
if len(v.Children) > 0 {
removals = fetchTeamRemovals(v.Children, removals, member)
}
}
return removals
}

func fetchRemovals(list []string, removals []string, member string, name string) []string {
for _, i := range list {
if i == member {
removals = append(removals, name)
}
}
return removals
}
28 changes: 28 additions & 0 deletions cmd/remove-members/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"reflect"
"testing"
)

func TestReadMemberList(t *testing.T) {
memberList, err := readMemberList("test/members.txt")
Copy link
Member

Choose a reason for hiding this comment

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

Would prefer you move this to testdata, it's more idiomatic (see go help test)

Suggested change
memberList, err := readMemberList("test/members.txt")
memberList, err := readMemberList("testdata/members.txt")

if err != nil {
t.Error(err)
}

expectedMemberList := []string{"johndoe", "janedoe"}

if !reflect.DeepEqual(memberList, expectedMemberList) {
t.Errorf("Values mismatch. Expected: %v Actual: %v", expectedMemberList, memberList)
}
}

func TestRemoveMembers(t *testing.T) {

memberList := []string{"johndoe", "janedoe"}

if err := removeMembers(memberList, "test"); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

So now that I've gotten here, stepping back a bit, it would be cool/ideal if this was all structured in such a way that you could write tests that:

  • don't interact with the filesystem
  • don't call git

The filesystem thing might be more effort than it's worth (mock out walk, pass in a tree). But the git thing... maybe write a git interface with add and commit methods, and pass in a mock in these tests?

https://github.com/kubernetes/test-infra/blob/master/prow/repoowners/repoowners_test.go would be an example of unit tests that dealt with files in a tree layout

t.Error(err)
}
}
2 changes: 2 additions & 0 deletions cmd/remove-members/test/members.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
johndoe
Copy link
Member

Choose a reason for hiding this comment

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

another test case to consider: a file not named orgs.yaml or teams.yaml that has a member's name in it

janedoe
3 changes: 3 additions & 0 deletions cmd/remove-members/test/org.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
members:
Copy link
Member

Choose a reason for hiding this comment

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

another test case to consider: an org.yaml that also has a teams entry

- loremipsum
- foobar
15 changes: 15 additions & 0 deletions cmd/remove-members/test/teams.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
teams:
Copy link
Member

Choose a reason for hiding this comment

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

another test case to consider: a teams.yaml in a subpath

test-team:
description: ""
members:
- loremipsum
- johndoe #some random comment
- foobar #some random comment 2
- janedoe
test-team-two:
description: ""
members:
- loremipsum
- johndoe
- foobar

2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ go 1.13
require (
github.com/ghodss/yaml v1.0.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/pflag v1.0.5
k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d
k8s.io/test-infra v0.0.0-20191222193732-de81526abe72
sigs.k8s.io/yaml v1.2.0 // indirect
)

// Pin all k8s.io staging repositories to kubernetes-1.15.3.
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down Expand Up @@ -494,6 +495,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down Expand Up @@ -537,4 +540,6 @@ sigs.k8s.io/controller-runtime v0.3.0/go.mod h1:Cw6PkEg0Sa7dAYovGT4R0tRkGhHXpYij
sigs.k8s.io/structured-merge-diff v0.0.0-20190302045857-e85c7b244fd2/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/testing_frameworks v0.1.1/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI=