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