diff --git a/cmd/remove-members/main.go b/cmd/remove-members/main.go new file mode 100644 index 0000000000..2c112fbe9a --- /dev/null +++ b/cmd/remove-members/main.go @@ -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 + 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 + 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 +} diff --git a/cmd/remove-members/main_test.go b/cmd/remove-members/main_test.go new file mode 100644 index 0000000000..40051d7750 --- /dev/null +++ b/cmd/remove-members/main_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestReadMemberList(t *testing.T) { + memberList, err := readMemberList("test/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 { + t.Error(err) + } +} diff --git a/cmd/remove-members/test/members.txt b/cmd/remove-members/test/members.txt new file mode 100644 index 0000000000..3e779f5785 --- /dev/null +++ b/cmd/remove-members/test/members.txt @@ -0,0 +1,2 @@ +johndoe +janedoe \ No newline at end of file diff --git a/cmd/remove-members/test/org.yaml b/cmd/remove-members/test/org.yaml new file mode 100644 index 0000000000..1e64c214fa --- /dev/null +++ b/cmd/remove-members/test/org.yaml @@ -0,0 +1,3 @@ +members: + - loremipsum + - foobar \ No newline at end of file diff --git a/cmd/remove-members/test/teams.yaml b/cmd/remove-members/test/teams.yaml new file mode 100644 index 0000000000..3126647686 --- /dev/null +++ b/cmd/remove-members/test/teams.yaml @@ -0,0 +1,15 @@ +teams: + test-team: + description: "" + members: + - loremipsum + - johndoe #some random comment + - foobar #some random comment 2 + - janedoe + test-team-two: + description: "" + members: + - loremipsum + - johndoe + - foobar + diff --git a/go.mod b/go.mod index 04a7439032..f5172b0525 100644 --- a/go.mod +++ b/go.mod @@ -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. diff --git a/go.sum b/go.sum index 3e3a312c3d..cd3d32a62c 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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=