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, >.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 }