github.com/abayer/test-infra@v0.0.5/prow/cmd/peribolos/main.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"net/url"
    24  	"os"
    25  	"strings"
    26  
    27  	"k8s.io/test-infra/prow/config"
    28  	"k8s.io/test-infra/prow/config/org"
    29  	"k8s.io/test-infra/prow/flagutil"
    30  	"k8s.io/test-infra/prow/github"
    31  	"k8s.io/test-infra/prow/logrusutil"
    32  
    33  	"github.com/ghodss/yaml"
    34  	"github.com/sirupsen/logrus"
    35  	"k8s.io/apimachinery/pkg/util/sets"
    36  )
    37  
    38  const (
    39  	defaultEndpoint  = "https://api.github.com"
    40  	defaultMinAdmins = 5
    41  	defaultDelta     = 0.25
    42  	defaultTokens    = 300
    43  	defaultBurst     = 100
    44  )
    45  
    46  type options struct {
    47  	config         string
    48  	confirm        bool
    49  	dump           string
    50  	endpoint       flagutil.Strings
    51  	jobConfig      string
    52  	maximumDelta   float64
    53  	minAdmins      int
    54  	requireSelf    bool
    55  	requiredAdmins flagutil.Strings
    56  	fixOrg         bool
    57  	fixOrgMembers  bool
    58  	fixTeamMembers bool
    59  	fixTeams       bool
    60  	token          string
    61  	tokenBurst     int
    62  	tokensPerHour  int
    63  }
    64  
    65  func parseOptions() options {
    66  	var o options
    67  	if err := o.parseArgs(flag.CommandLine, os.Args[1:]); err != nil {
    68  		logrus.Fatalf("Invalid flags: %v", err)
    69  	}
    70  	return o
    71  }
    72  
    73  func (o *options) parseArgs(flags *flag.FlagSet, args []string) error {
    74  	o.endpoint = flagutil.NewStrings(defaultEndpoint)
    75  	flags.Var(&o.endpoint, "github-endpoint", "Github api endpoint, may differ for enterprise")
    76  	o.requiredAdmins = flagutil.NewStrings()
    77  	flags.Var(&o.requiredAdmins, "required-admins", "Ensure config specifies these users as admins")
    78  	flags.IntVar(&o.minAdmins, "min-admins", defaultMinAdmins, "Ensure config specifies at least this many admins")
    79  	flags.BoolVar(&o.requireSelf, "require-self", true, "Ensure --github-token-path user is an admin")
    80  	flags.Float64Var(&o.maximumDelta, "maximum-removal-delta", defaultDelta, "Fail if config removes more than this fraction of current members")
    81  	flags.StringVar(&o.config, "config-path", "", "Path to prow config.yaml")
    82  	flags.StringVar(&o.jobConfig, "job-config-path", "", "Path to prow job configs.")
    83  	flags.BoolVar(&o.confirm, "confirm", false, "Mutate github if set")
    84  	flags.StringVar(&o.token, "github-token-path", "", "Path to github token")
    85  	flags.IntVar(&o.tokensPerHour, "tokens", defaultTokens, "Throttle hourly token consumption (0 to disable)")
    86  	flags.IntVar(&o.tokenBurst, "token-burst", defaultBurst, "Allow consuming a subset of hourly tokens in a short burst")
    87  	flags.StringVar(&o.dump, "dump", "", "Output current config of this org if set")
    88  	flags.BoolVar(&o.fixOrg, "fix-org", false, "Change org metadata if set")
    89  	flags.BoolVar(&o.fixOrgMembers, "fix-org-members", false, "Add/remove org members if set")
    90  	flags.BoolVar(&o.fixTeams, "fix-teams", false, "Create/delete/update teams if set")
    91  	flags.BoolVar(&o.fixTeamMembers, "fix-team-members", false, "Add/remove team members if set")
    92  
    93  	if err := flags.Parse(args); err != nil {
    94  		return err
    95  	}
    96  	if o.token == "" {
    97  		return errors.New("empty --github-token-path")
    98  	}
    99  	if o.tokensPerHour > 0 && o.tokenBurst >= o.tokensPerHour {
   100  		return fmt.Errorf("--tokens=%d must exceed --token-burst=%d", o.tokensPerHour, o.tokenBurst)
   101  	}
   102  
   103  	for _, ep := range o.endpoint.Strings() {
   104  		_, err := url.Parse(ep)
   105  		if err != nil {
   106  			return fmt.Errorf("invalid --endpoint URL %q: %v", ep, err)
   107  		}
   108  	}
   109  	if o.minAdmins < 2 {
   110  		return fmt.Errorf("--min-admins=%d must be at least 2", o.minAdmins)
   111  	}
   112  	if o.maximumDelta > 1 || o.maximumDelta < 0 {
   113  		return fmt.Errorf("--maximum-removal-delta=%f must be a non-negative number less than 1.0", o.maximumDelta)
   114  	}
   115  
   116  	if o.confirm && o.dump != "" {
   117  		return fmt.Errorf("--confirm cannot be used with --dump=%s", o.dump)
   118  	}
   119  	if o.config == "" && o.dump == "" {
   120  		return errors.New("--config or --dump required")
   121  	}
   122  	if o.config != "" && o.dump != "" {
   123  		return fmt.Errorf("--config-path=%s and --dump=%s cannot both be set", o.config, o.dump)
   124  	}
   125  
   126  	if o.fixTeamMembers && !o.fixTeams {
   127  		return fmt.Errorf("--fix-team-members requires --fix-teams")
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  func main() {
   134  	logrus.SetFormatter(
   135  		logrusutil.NewDefaultFieldsFormatter(nil, logrus.Fields{"component": "peribolos"}),
   136  	)
   137  	o := parseOptions()
   138  
   139  	var c *github.Client
   140  
   141  	secretAgent := &config.SecretAgent{}
   142  	if err := secretAgent.Start([]string{o.token}); err != nil {
   143  		logrus.WithError(err).Fatal("Error starting secrets agent.")
   144  	}
   145  
   146  	if o.confirm {
   147  		c = github.NewClient(secretAgent.GetTokenGenerator(o.token), o.endpoint.Strings()...)
   148  	} else {
   149  		c = github.NewDryRunClient(secretAgent.GetTokenGenerator(o.token), o.endpoint.Strings()...)
   150  	}
   151  	if o.tokensPerHour > 0 {
   152  		c.Throttle(o.tokensPerHour, o.tokenBurst) // 300 hourly tokens, bursts of 100 (default)
   153  	}
   154  
   155  	if o.dump != "" {
   156  		ret, err := dumpOrgConfig(c, o.dump)
   157  		if err != nil {
   158  			logrus.WithError(err).Fatalf("Dump %s failed to collect current data.", o.dump)
   159  		}
   160  		out, err := yaml.Marshal(ret)
   161  		if err != nil {
   162  			logrus.WithError(err).Fatalf("Dump %s failed to marshal output.", o.dump)
   163  		}
   164  		logrus.Info("Dumping orgs[\"%s\"]:", o.dump)
   165  		fmt.Println(string(out))
   166  		return
   167  	}
   168  
   169  	cfg, err := config.Load(o.config, o.jobConfig)
   170  	if err != nil {
   171  		logrus.Fatalf("Failed to load --config=%s: %v", o.config, err)
   172  	}
   173  
   174  	for name, orgcfg := range cfg.Orgs {
   175  		if err := configureOrg(o, c, name, orgcfg); err != nil {
   176  			logrus.Fatalf("Configuration failed: %v", err)
   177  		}
   178  	}
   179  }
   180  
   181  type dumpClient interface {
   182  	GetOrg(name string) (*github.Organization, error)
   183  	ListOrgMembers(org, role string) ([]github.TeamMember, error)
   184  	ListTeams(org string) ([]github.Team, error)
   185  	ListTeamMembers(id int, role string) ([]github.TeamMember, error)
   186  }
   187  
   188  func dumpOrgConfig(client dumpClient, orgName string) (*org.Config, error) {
   189  	out := org.Config{
   190  		Members: []string{},
   191  		Admins:  []string{},
   192  	}
   193  	meta, err := client.GetOrg(orgName)
   194  	if err != nil {
   195  		return nil, fmt.Errorf("failed to get org: %v", err)
   196  	}
   197  	out.Metadata.BillingEmail = &meta.BillingEmail
   198  	out.Metadata.Company = &meta.Company
   199  	out.Metadata.Email = &meta.Email
   200  	out.Metadata.Name = &meta.Name
   201  	out.Metadata.Description = &meta.Description
   202  	out.Metadata.Location = &meta.Location
   203  	out.Metadata.HasOrganizationProjects = &meta.HasOrganizationProjects
   204  	out.Metadata.HasRepositoryProjects = &meta.HasRepositoryProjects
   205  	drp := org.RepoPermissionLevel(meta.DefaultRepositoryPermission)
   206  	out.Metadata.DefaultRepositoryPermission = &drp
   207  	out.Metadata.MembersCanCreateRepositories = &meta.MembersCanCreateRepositories
   208  
   209  	admins, err := client.ListOrgMembers(orgName, github.RoleAdmin)
   210  	if err != nil {
   211  		return nil, fmt.Errorf("failed to list org admins: %v", err)
   212  	}
   213  	for _, m := range admins {
   214  		out.Admins = append(out.Admins, m.Login)
   215  	}
   216  
   217  	orgMembers, err := client.ListOrgMembers(orgName, github.RoleMember)
   218  	if err != nil {
   219  		return nil, fmt.Errorf("failed to list org members: %v", err)
   220  	}
   221  	for _, m := range orgMembers {
   222  		out.Members = append(out.Members, m.Login)
   223  	}
   224  
   225  	teams, err := client.ListTeams(orgName)
   226  	if err != nil {
   227  		return nil, fmt.Errorf("failed to list teams: %v", err)
   228  	}
   229  
   230  	names := map[int]string{}   // what's the name of a team?
   231  	idMap := map[int]org.Team{} // metadata for a team
   232  	children := map[int][]int{} // what children does it have
   233  	var tops []int              // what are the top-level teams
   234  
   235  	for _, t := range teams {
   236  		p := org.Privacy(t.Privacy)
   237  		d := t.Description
   238  		nt := org.Team{
   239  			TeamMetadata: org.TeamMetadata{
   240  				Description: &d,
   241  				Privacy:     &p,
   242  			},
   243  			Maintainers: []string{},
   244  			Members:     []string{},
   245  			Children:    map[string]org.Team{},
   246  		}
   247  		maintainers, err := client.ListTeamMembers(t.ID, github.RoleMaintainer)
   248  		if err != nil {
   249  			return nil, fmt.Errorf("failed to list team %d(%s) maintainers: %v", t.ID, t.Name, err)
   250  		}
   251  		for _, m := range maintainers {
   252  			nt.Maintainers = append(nt.Maintainers, m.Login)
   253  		}
   254  		teamMembers, err := client.ListTeamMembers(t.ID, github.RoleMember)
   255  		if err != nil {
   256  			return nil, fmt.Errorf("failed to list team %d(%s) members: %v", t.ID, t.Name, err)
   257  		}
   258  		for _, m := range teamMembers {
   259  			nt.Members = append(nt.Members, m.Login)
   260  		}
   261  
   262  		names[t.ID] = t.Name
   263  		idMap[t.ID] = nt
   264  
   265  		if t.Parent == nil { // top level team
   266  			tops = append(tops, t.ID)
   267  		} else { // add this id to the list of the parent's children
   268  			children[t.Parent.ID] = append(children[t.Parent.ID], t.ID)
   269  		}
   270  	}
   271  
   272  	var makeChild func(id int) org.Team
   273  	makeChild = func(id int) org.Team {
   274  		t := idMap[id]
   275  		for _, cid := range children[id] {
   276  			child := makeChild(cid)
   277  			t.Children[names[cid]] = child
   278  		}
   279  		return t
   280  	}
   281  
   282  	out.Teams = make(map[string]org.Team, len(tops))
   283  	for _, id := range tops {
   284  		out.Teams[names[id]] = makeChild(id)
   285  	}
   286  
   287  	return &out, nil
   288  }
   289  
   290  type orgClient interface {
   291  	BotName() (string, error)
   292  	ListOrgMembers(org, role string) ([]github.TeamMember, error)
   293  	ListOrgInvitations(org string) ([]github.OrgInvitation, error)
   294  	RemoveOrgMembership(org, user string) error
   295  	UpdateOrgMembership(org, user string, admin bool) (*github.OrgMembership, error)
   296  }
   297  
   298  func configureOrgMembers(opt options, client orgClient, orgName string, orgConfig org.Config) error {
   299  	// Get desired state
   300  	wantAdmins := sets.NewString(orgConfig.Admins...)
   301  	wantMembers := sets.NewString(orgConfig.Members...)
   302  
   303  	// Sanity desired state
   304  	if n := len(wantAdmins); n < opt.minAdmins {
   305  		return fmt.Errorf("%s must specify at least %d admins, only found %d", orgName, opt.minAdmins, n)
   306  	}
   307  	var missing []string
   308  	for _, r := range opt.requiredAdmins.Strings() {
   309  		if !wantAdmins.Has(r) {
   310  			missing = append(missing, r)
   311  		}
   312  	}
   313  	if len(missing) > 0 {
   314  		return fmt.Errorf("%s must specify %v as admins, missing %v", orgName, opt.requiredAdmins, missing)
   315  	}
   316  	if opt.requireSelf {
   317  		if me, err := client.BotName(); err != nil {
   318  			return fmt.Errorf("cannot determine user making requests for %s: %v", opt.token, err)
   319  		} else if !wantAdmins.Has(me) {
   320  			return fmt.Errorf("authenticated user %s is not an admin of %s", me, orgName)
   321  		}
   322  	}
   323  
   324  	// Get current state
   325  	haveAdmins := sets.String{}
   326  	haveMembers := sets.String{}
   327  	ms, err := client.ListOrgMembers(orgName, github.RoleAdmin)
   328  	if err != nil {
   329  		return fmt.Errorf("failed to list %s admins: %v", orgName, err)
   330  	}
   331  	for _, m := range ms {
   332  		haveAdmins.Insert(m.Login)
   333  	}
   334  	if ms, err = client.ListOrgMembers(orgName, github.RoleMember); err != nil {
   335  		return fmt.Errorf("failed to list %s members: %v", orgName, err)
   336  	}
   337  	for _, m := range ms {
   338  		haveMembers.Insert(m.Login)
   339  	}
   340  	is, err := client.ListOrgInvitations(orgName)
   341  	if err != nil {
   342  		return fmt.Errorf("failed to list %s invitations: %v", orgName, err)
   343  	}
   344  	for _, i := range is { // Do not reinvite anyone
   345  		haveMembers.Insert(i.Login)
   346  		haveAdmins.Insert(i.Login)
   347  	}
   348  
   349  	have := memberships{members: haveMembers, super: haveAdmins}
   350  	want := memberships{members: wantMembers, super: wantAdmins}
   351  	have.normalize()
   352  	want.normalize()
   353  	// Figure out who to remove
   354  	remove := have.all().Difference(want.all())
   355  
   356  	// Sanity check changes
   357  	if d := float64(len(remove)) / float64(len(have.all())); d > opt.maximumDelta {
   358  		return fmt.Errorf("cannot delete %d memberships or %.3f of %s (exceeds limit of %.3f)", len(remove), d, orgName, opt.maximumDelta)
   359  	}
   360  
   361  	teamMembers := sets.String{}
   362  	teamNames := sets.String{}
   363  	duplicateTeamNames := sets.String{}
   364  	for name, team := range orgConfig.Teams {
   365  		teamMembers.Insert(team.Members...)
   366  		teamMembers.Insert(team.Maintainers...)
   367  		if teamNames.Has(name) {
   368  			duplicateTeamNames.Insert(name)
   369  		}
   370  		teamNames.Insert(name)
   371  		for _, n := range team.Previously {
   372  			if teamNames.Has(n) {
   373  				duplicateTeamNames.Insert(n)
   374  			}
   375  			teamNames.Insert(n)
   376  		}
   377  	}
   378  
   379  	if outside := teamMembers.Difference(want.all()); len(outside) > 0 {
   380  		return fmt.Errorf("all team members/maintainers must also be org members: %s", strings.Join(outside.List(), ", "))
   381  	}
   382  
   383  	if n := len(duplicateTeamNames); n > 0 {
   384  		return fmt.Errorf("team names must be unique (including previous names), %d duplicated names: %s", n, strings.Join(duplicateTeamNames.List(), ", "))
   385  	}
   386  
   387  	adder := func(user string, super bool) error {
   388  		role := github.RoleMember
   389  		if super {
   390  			role = github.RoleAdmin
   391  		}
   392  		om, err := client.UpdateOrgMembership(orgName, user, super)
   393  		if err != nil {
   394  			logrus.WithError(err).Warnf("UpdateOrgMembership(%s, %s, %t) failed", orgName, user, super)
   395  		} else if om.State == github.StatePending {
   396  			logrus.Infof("Invited %s to %s as a %s", user, orgName, role)
   397  		} else {
   398  			logrus.Infof("Set %s as a %s of %s", user, role, orgName)
   399  		}
   400  		return err
   401  	}
   402  
   403  	remover := func(user string) error {
   404  		err := client.RemoveOrgMembership(orgName, user)
   405  		if err != nil {
   406  			logrus.WithError(err).Warnf("RemoveOrgMembership(%s, %s) failed", orgName, user)
   407  		}
   408  		return err
   409  	}
   410  
   411  	return configureMembers(have, want, adder, remover)
   412  }
   413  
   414  type memberships struct {
   415  	members sets.String
   416  	super   sets.String
   417  }
   418  
   419  func (m memberships) all() sets.String {
   420  	return m.members.Union(m.super)
   421  }
   422  
   423  func normalize(s sets.String) sets.String {
   424  	out := sets.String{}
   425  	for i := range s {
   426  		out.Insert(github.NormLogin(i))
   427  	}
   428  	return out
   429  }
   430  
   431  func (m *memberships) normalize() {
   432  	m.members = normalize(m.members)
   433  	m.super = normalize(m.super)
   434  }
   435  
   436  func configureMembers(have, want memberships, adder func(user string, super bool) error, remover func(user string) error) error {
   437  	have.normalize()
   438  	want.normalize()
   439  	if both := want.super.Intersection(want.members); len(both) > 0 {
   440  		return fmt.Errorf("users in both roles: %s", strings.Join(both.List(), ", "))
   441  	}
   442  	remove := have.all().Difference(want.all())
   443  	members := want.members.Difference(have.members)
   444  	supers := want.super.Difference(have.super)
   445  
   446  	var errs []error
   447  	for u := range members {
   448  		if err := adder(u, false); err != nil {
   449  			errs = append(errs, err)
   450  		}
   451  	}
   452  	for u := range supers {
   453  		if err := adder(u, true); err != nil {
   454  			errs = append(errs, err)
   455  		}
   456  	}
   457  
   458  	for u := range remove {
   459  		if err := remover(u); err != nil {
   460  			errs = append(errs, err)
   461  		}
   462  	}
   463  
   464  	if n := len(errs); n > 0 {
   465  		return fmt.Errorf("%d errors: %v", n, errs)
   466  	}
   467  	return nil
   468  }
   469  
   470  // findTeam returns teams[n] for the first n in [name, previousNames, ...] that is in teams.
   471  func findTeam(teams map[string]github.Team, name string, previousNames ...string) *github.Team {
   472  	if t, ok := teams[name]; ok {
   473  		return &t
   474  	}
   475  	for _, p := range previousNames {
   476  		if t, ok := teams[p]; ok {
   477  			return &t
   478  		}
   479  	}
   480  	return nil
   481  }
   482  
   483  // validateTeamNames returns an error if any current/previous names are used multiple times in the config.
   484  func validateTeamNames(orgConfig org.Config) error {
   485  	// Does the config duplicate any team names?
   486  	used := sets.String{}
   487  	dups := sets.String{}
   488  	for name, orgTeam := range orgConfig.Teams {
   489  		if used.Has(name) {
   490  			dups.Insert(name)
   491  		} else {
   492  			used.Insert(name)
   493  		}
   494  		for _, n := range orgTeam.Previously {
   495  			if used.Has(n) {
   496  				dups.Insert(n)
   497  			} else {
   498  				used.Insert(n)
   499  			}
   500  		}
   501  	}
   502  	if n := len(dups); n > 0 {
   503  		return fmt.Errorf("%d duplicated names: %s", n, strings.Join(dups.List(), ", "))
   504  	}
   505  	return nil
   506  }
   507  
   508  type teamClient interface {
   509  	ListTeams(org string) ([]github.Team, error)
   510  	CreateTeam(org string, team github.Team) (*github.Team, error)
   511  	DeleteTeam(id int) error
   512  }
   513  
   514  // configureTeams returns the ids for all expected team names, creating/deleting teams as necessary.
   515  func configureTeams(client teamClient, orgName string, orgConfig org.Config, maxDelta float64) (map[string]github.Team, error) {
   516  	if err := validateTeamNames(orgConfig); err != nil {
   517  		return nil, err
   518  	}
   519  
   520  	// What teams exist?
   521  	ids := map[int]github.Team{}
   522  	ints := sets.Int{}
   523  	teamList, err := client.ListTeams(orgName)
   524  	if err != nil {
   525  		return nil, fmt.Errorf("failed to list teams: %v", err)
   526  	}
   527  	for _, t := range teamList {
   528  		ids[t.ID] = t
   529  		ints.Insert(t.ID)
   530  	}
   531  
   532  	// What is the lowest ID for each team?
   533  	older := map[string][]github.Team{}
   534  	names := map[string]github.Team{}
   535  	for _, t := range ids {
   536  		n := t.Name
   537  		switch val, ok := names[n]; {
   538  		case !ok: // first occurrence of the name
   539  			names[n] = t
   540  		case ok && t.ID < val.ID: // t has the lower ID, replace and send current to older set
   541  			names[n] = t
   542  			older[n] = append(older[n], val)
   543  		default: // t does not have smallest id, add it to older set
   544  			older[n] = append(older[n], val)
   545  		}
   546  	}
   547  
   548  	// What team are we using for each configured name, and which names are missing?
   549  	matches := map[string]github.Team{}
   550  	missing := map[string]org.Team{}
   551  	used := sets.Int{}
   552  	var match func(teams map[string]org.Team)
   553  	match = func(teams map[string]org.Team) {
   554  		for name, orgTeam := range teams {
   555  			match(orgTeam.Children)
   556  			t := findTeam(names, name, orgTeam.Previously...)
   557  			if t == nil {
   558  				missing[name] = orgTeam
   559  				continue
   560  			}
   561  			matches[name] = *t // t.Name != name if we matched on orgTeam.Previously
   562  			used.Insert(t.ID)
   563  		}
   564  	}
   565  	match(orgConfig.Teams)
   566  
   567  	// First compute teams we will delete, ensure we are not deleting too many
   568  	unused := ints.Difference(used)
   569  	if delta := float64(len(unused)) / float64(len(ints)); delta > maxDelta {
   570  		return nil, fmt.Errorf("cannot delete %d teams or %.3f of %s teams (exceeds limit of %.3f)", len(unused), delta, orgName, maxDelta)
   571  	}
   572  
   573  	// Create any missing team names
   574  	var failures []string
   575  	for name, orgTeam := range missing {
   576  		t := &github.Team{Name: name}
   577  		if orgTeam.Description != nil {
   578  			t.Description = *orgTeam.Description
   579  		}
   580  		if orgTeam.Privacy != nil {
   581  			t.Privacy = string(*orgTeam.Privacy)
   582  		}
   583  		t, err := client.CreateTeam(orgName, *t)
   584  		if err != nil {
   585  			logrus.WithError(err).Warnf("Failed to create %s in %s", name, orgName)
   586  			failures = append(failures, name)
   587  			continue
   588  		}
   589  		matches[name] = *t
   590  		// t.ID may include an ID already present in ints if other actors are deleting teams.
   591  		used.Insert(t.ID)
   592  	}
   593  	if n := len(failures); n > 0 {
   594  		return nil, fmt.Errorf("failed to create %d teams: %s", n, strings.Join(failures, ", "))
   595  	}
   596  
   597  	// Remove any IDs returned by CreateTeam() that are in the unused set.
   598  	if reused := unused.Intersection(used); len(reused) > 0 {
   599  		// Logically possible for:
   600  		// * another actor to delete team N after the ListTeams() call
   601  		// * github to reuse team N after someone deted it
   602  		// Therefore used may now include IDs in unused, handle this situation.
   603  		logrus.Warnf("Will not delete %d team IDs reused by github: %v", len(reused), reused.List())
   604  		unused = unused.Difference(reused)
   605  	}
   606  	// Delete undeclared teams.
   607  	for id := range unused {
   608  		if err := client.DeleteTeam(id); err != nil {
   609  			str := fmt.Sprintf("%d(%s)", id, ids[id].Name)
   610  			logrus.WithError(err).Warnf("Failed to delete team %s from %s", str, orgName)
   611  			failures = append(failures, str)
   612  		}
   613  	}
   614  	if n := len(failures); n > 0 {
   615  		return nil, fmt.Errorf("failed to delete %d teams: %s", n, strings.Join(failures, ", "))
   616  	}
   617  
   618  	// Return matches
   619  	return matches, nil
   620  }
   621  
   622  // updateString will return true and set have to want iff they are set and different.
   623  func updateString(have, want *string) bool {
   624  	switch {
   625  	case have == nil:
   626  		panic("have must be non-nil")
   627  	case want == nil:
   628  		return false // do not care what we have
   629  	case *have == *want:
   630  		return false // already have it
   631  	}
   632  	*have = *want // update value
   633  	return true
   634  }
   635  
   636  // updateBool will return true and set have to want iff they are set and different.
   637  func updateBool(have, want *bool) bool {
   638  	switch {
   639  	case have == nil:
   640  		panic("have must not be nil")
   641  	case want == nil:
   642  		return false // do not care what we have
   643  	case *have == *want:
   644  		return false //already have it
   645  	}
   646  	*have = *want // update value
   647  	return true
   648  }
   649  
   650  type orgMetadataClient interface {
   651  	GetOrg(name string) (*github.Organization, error)
   652  	EditOrg(name string, org github.Organization) (*github.Organization, error)
   653  }
   654  
   655  // configureOrgMeta will update github to have the non-nil wanted metadata values.
   656  func configureOrgMeta(client orgMetadataClient, orgName string, want org.Metadata) error {
   657  	cur, err := client.GetOrg(orgName)
   658  	if err != nil {
   659  		return fmt.Errorf("failed to get %s metadata: %v", orgName, err)
   660  	}
   661  	change := false
   662  	change = updateString(&cur.BillingEmail, want.BillingEmail) || change
   663  	change = updateString(&cur.Company, want.Company) || change
   664  	change = updateString(&cur.Email, want.Email) || change
   665  	change = updateString(&cur.Name, want.Name) || change
   666  	change = updateString(&cur.Description, want.Description) || change
   667  	change = updateString(&cur.Location, want.Location) || change
   668  	if want.DefaultRepositoryPermission != nil {
   669  		w := string(*want.DefaultRepositoryPermission)
   670  		change = updateString(&cur.DefaultRepositoryPermission, &w)
   671  	}
   672  	change = updateBool(&cur.HasOrganizationProjects, want.HasOrganizationProjects) || change
   673  	change = updateBool(&cur.HasRepositoryProjects, want.HasRepositoryProjects) || change
   674  	change = updateBool(&cur.MembersCanCreateRepositories, want.MembersCanCreateRepositories) || change
   675  	if change {
   676  		if _, err := client.EditOrg(orgName, *cur); err != nil {
   677  			return fmt.Errorf("failed to edit %s metadata: %v", orgName, err)
   678  		}
   679  	}
   680  	return nil
   681  }
   682  
   683  func configureOrg(opt options, client *github.Client, orgName string, orgConfig org.Config) error {
   684  	// Ensure that metadata is configured correctly.
   685  	if !opt.fixOrg {
   686  		logrus.Infof("Skipping org metadata configuration")
   687  	} else if err := configureOrgMeta(client, orgName, orgConfig.Metadata); err != nil {
   688  		return err
   689  	}
   690  
   691  	// Invite/remove/update members to the org.
   692  	if !opt.fixOrgMembers {
   693  		logrus.Infof("Skipping org member configuration")
   694  	} else if err := configureOrgMembers(opt, client, orgName, orgConfig); err != nil {
   695  		return fmt.Errorf("failed to configure %s members: %v", orgName, err)
   696  	}
   697  
   698  	if !opt.fixTeams {
   699  		logrus.Infof("Skipping team and team member configuration")
   700  		return nil
   701  	}
   702  
   703  	// Find the id and current state of each declared team (create/delete as necessary)
   704  	githubTeams, err := configureTeams(client, orgName, orgConfig, opt.maximumDelta)
   705  	if err != nil {
   706  		return fmt.Errorf("failed to configure %s teams: %v", orgName, err)
   707  	}
   708  
   709  	for name, team := range orgConfig.Teams {
   710  		err := configureTeamAndMembers(client, opt.fixTeamMembers, githubTeams, name, orgName, team, nil)
   711  		if err != nil {
   712  			return fmt.Errorf("failed to configure %s teams: %v", orgName, err)
   713  		}
   714  	}
   715  	return nil
   716  }
   717  
   718  func configureTeamAndMembers(client *github.Client, fixMembers bool, githubTeams map[string]github.Team, name, orgName string, team org.Team, parent *int) error {
   719  	gt, ok := githubTeams[name]
   720  	if !ok { // configureTeams is buggy if this is the case
   721  		return fmt.Errorf("%s not found in id list", name)
   722  	}
   723  
   724  	// Configure team metadata
   725  	err := configureTeam(client, orgName, name, team, gt, parent)
   726  	if err != nil {
   727  		return fmt.Errorf("failed to update %s metadata: %v", name, err)
   728  	}
   729  
   730  	// Configure team members
   731  	if !fixMembers {
   732  		logrus.Infof("Skipping %s member configuration", name)
   733  	} else if err = configureTeamMembers(client, gt.ID, team); err != nil {
   734  		return fmt.Errorf("failed to update %s members: %v", name, err)
   735  	}
   736  
   737  	for childName, childTeam := range team.Children {
   738  		err = configureTeamAndMembers(client, fixMembers, githubTeams, childName, orgName, childTeam, &gt.ID)
   739  		if err != nil {
   740  			return fmt.Errorf("failed to update %s child teams: %v", name, err)
   741  		}
   742  	}
   743  
   744  	return nil
   745  }
   746  
   747  type editTeamClient interface {
   748  	EditTeam(team github.Team) (*github.Team, error)
   749  }
   750  
   751  // configureTeam patches the team name/description/privacy when values differ
   752  func configureTeam(client editTeamClient, orgName, teamName string, team org.Team, gt github.Team, parent *int) error {
   753  	// Do we need to reconfigure any team settings?
   754  	patch := false
   755  	if gt.Name != teamName {
   756  		patch = true
   757  	}
   758  	gt.Name = teamName
   759  	if team.Description != nil && gt.Description != *team.Description {
   760  		patch = true
   761  		gt.Description = *team.Description
   762  	} else {
   763  		gt.Description = ""
   764  	}
   765  	// doesn't have parent in github, but has parent in config
   766  	if gt.Parent == nil && parent != nil {
   767  		patch = true
   768  		gt.ParentTeamID = parent
   769  	}
   770  	if gt.Parent != nil { // has parent in github ...
   771  		if parent == nil { // ... but doesn't need one
   772  			patch = true
   773  			gt.Parent = nil
   774  			gt.ParentTeamID = parent
   775  		} else if gt.Parent.ID != *parent { // but it's different than the config
   776  			patch = true
   777  			gt.Parent = nil
   778  			gt.ParentTeamID = parent
   779  		}
   780  	}
   781  
   782  	if team.Privacy != nil && gt.Privacy != string(*team.Privacy) {
   783  		patch = true
   784  		gt.Privacy = string(*team.Privacy)
   785  
   786  	} else if team.Privacy == nil && (parent != nil || len(team.Children) > 0) && gt.Privacy != "closed" {
   787  		patch = true
   788  		gt.Privacy = github.PrivacyClosed // nested teams must be closed
   789  	}
   790  
   791  	if patch { // yes we need to patch
   792  		if _, err := client.EditTeam(gt); err != nil {
   793  			return fmt.Errorf("failed to edit %s team %d(%s): %v", orgName, gt.ID, gt.Name, err)
   794  		}
   795  	}
   796  	return nil
   797  }
   798  
   799  // teamMembersClient can list/remove/update people to a team.
   800  type teamMembersClient interface {
   801  	ListTeamMembers(id int, role string) ([]github.TeamMember, error)
   802  	RemoveTeamMembership(id int, user string) error
   803  	UpdateTeamMembership(id int, user string, maintainer bool) (*github.TeamMembership, error)
   804  }
   805  
   806  // configureTeamMembers will add/update people to the appropriate role on the team, and remove anyone else.
   807  func configureTeamMembers(client teamMembersClient, id int, team org.Team) error {
   808  	// Get desired state
   809  	wantMaintainers := sets.NewString(team.Maintainers...)
   810  	wantMembers := sets.NewString(team.Members...)
   811  
   812  	// Get current state
   813  	haveMaintainers := sets.String{}
   814  	haveMembers := sets.String{}
   815  
   816  	members, err := client.ListTeamMembers(id, github.RoleMember)
   817  	if err != nil {
   818  		return fmt.Errorf("failed to list %d members: %v", id, err)
   819  	}
   820  	for _, m := range members {
   821  		haveMembers.Insert(m.Login)
   822  	}
   823  
   824  	maintainers, err := client.ListTeamMembers(id, github.RoleMaintainer)
   825  	if err != nil {
   826  		return fmt.Errorf("failed to list %d maintainers: %v", id, err)
   827  	}
   828  	for _, m := range maintainers {
   829  		haveMaintainers.Insert(m.Login)
   830  	}
   831  
   832  	adder := func(user string, super bool) error {
   833  		role := github.RoleMember
   834  		if super {
   835  			role = github.RoleMaintainer
   836  		}
   837  		tm, err := client.UpdateTeamMembership(id, user, super)
   838  		if err != nil {
   839  			logrus.WithError(err).Warnf("UpdateTeamMembership(%d, %s, %t) failed", id, user, super)
   840  		} else if tm.State == github.StatePending {
   841  			logrus.Infof("Invited %s to %d as a %s", user, id, role)
   842  		} else {
   843  			logrus.Infof("Set %s as a %s of %d", user, role, id)
   844  		}
   845  		return err
   846  	}
   847  
   848  	remover := func(user string) error {
   849  		err := client.RemoveTeamMembership(id, user)
   850  		if err != nil {
   851  			logrus.WithError(err).Warnf("RemoveTeamMembership(%d, %s) failed", id, user)
   852  		} else {
   853  			logrus.Infof("Removed %s from team %d", user, id)
   854  		}
   855  		return err
   856  	}
   857  
   858  	want := memberships{members: wantMembers, super: wantMaintainers}
   859  	have := memberships{members: haveMembers, super: haveMaintainers}
   860  	return configureMembers(have, want, adder, remover)
   861  }