github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/tackle/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 context2 "context" 21 "crypto/rand" 22 "errors" 23 "flag" 24 "fmt" 25 "io" 26 "net/url" 27 "os" 28 "os/exec" 29 "sort" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/sirupsen/logrus" 35 corev1 "k8s.io/api/core/v1" 36 networking "k8s.io/api/networking/v1" 37 apierrors "k8s.io/apimachinery/pkg/api/errors" 38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39 "k8s.io/client-go/kubernetes" 40 "k8s.io/client-go/rest" 41 "k8s.io/client-go/tools/clientcmd" 42 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 43 44 "sigs.k8s.io/prow/pkg/config/secret" 45 "sigs.k8s.io/prow/pkg/flagutil" 46 "sigs.k8s.io/prow/pkg/github" 47 48 _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // for gcp auth provider 49 ) 50 51 // printArray prints items in collection (up to a non-zero limit) and return a bool indicating if results were truncated. 52 func printArray(collection []string, limit int) bool { 53 for idx, item := range collection { 54 if limit > 0 && idx == limit { 55 break 56 } 57 fmt.Println(" *", item) 58 } 59 60 if limit > 0 && len(collection) > limit { 61 return true 62 } 63 64 return false 65 } 66 67 // validateNotEmpty handles validation that a collection is non-empty. 68 func validateNotEmpty(collection []string) bool { 69 return len(collection) > 0 70 } 71 72 // validateContainment handles validation for containment of target in collection. 73 func validateContainment(collection []string, target string) bool { 74 for _, val := range collection { 75 if val == target { 76 return true 77 } 78 } 79 80 return false 81 } 82 83 // prompt prompts user with a message (and optional default value); return the selection as string. 84 func prompt(promptMsg string, defaultVal string) string { 85 var choice string 86 87 if defaultVal != "" { 88 fmt.Printf("%s [%s]: ", promptMsg, defaultVal) 89 } else { 90 fmt.Printf("%s: ", promptMsg) 91 } 92 93 fmt.Scanln(&choice) 94 95 // If no `choice` and a `default`, then use `default` 96 if choice == "" { 97 return defaultVal 98 } 99 100 return choice 101 } 102 103 // ensure will ensure binary is on path or return an error with install message. 104 func ensure(binary, install string) error { 105 if _, err := exec.LookPath(binary); err != nil { 106 return fmt.Errorf("%s: %s", binary, install) 107 } 108 return nil 109 } 110 111 // ensureKubectl ensures kubectl is on path or prints a note of how to install. 112 func ensureKubectl() error { 113 return ensure("kubectl", "gcloud components install kubectl") 114 } 115 116 // ensureGcloud ensures gcloud on path or prints a note of how to install. 117 func ensureGcloud() error { 118 return ensure("gcloud", "https://cloud.google.com/sdk/gcloud") 119 } 120 121 // output returns the trimmed output of running args, or an err on non-zero exit. 122 func output(args ...string) (string, error) { 123 cmd := exec.Command(args[0], args[1:]...) 124 cmd.Stderr = os.Stderr 125 cmd.Stdin = os.Stdin 126 b, err := cmd.Output() 127 return strings.TrimSpace(string(b)), err 128 } 129 130 // currentAccount returns the configured account for gcloud 131 func currentAccount() (string, error) { 132 return output("gcloud", "config", "get-value", "core/account") 133 } 134 135 // currentProject returns the configured project for gcloud 136 func currentProject() (string, error) { 137 return output("gcloud", "config", "get-value", "core/project") 138 } 139 140 // currentZone returns the configured zone for gcloud 141 func currentZone() (string, error) { 142 return output("gcloud", "config", "get-value", "compute/zone") 143 } 144 145 // projects returns the list of accessible gcp projects 146 func projects(max int) ([]string, error) { 147 out, err := output("gcloud", "projects", "list", fmt.Sprintf("--limit=%d", max), "--format=value(project_id)") 148 if err != nil { 149 return nil, err 150 } 151 return strings.Split(out, "\n"), nil 152 } 153 154 // zones returns the list of accessible gcp zones 155 func zones() ([]string, error) { 156 out, err := output("gcloud", "compute", "zones", "list", "--format=value(name)") 157 if err != nil { 158 return nil, err 159 } 160 return strings.Split(out, "\n"), nil 161 } 162 163 // selectProject returns the user-selected project, defaulting to the current gcloud one. 164 func selectProject(choice string) (string, error) { 165 fmt.Print("Getting active GCP account...") 166 who, err := currentAccount() 167 if err != nil { 168 logrus.Warn("Run gcloud auth login to initialize gcloud") 169 return "", err 170 } 171 fmt.Println(who) 172 173 var projs []string 174 175 if choice == "" { 176 fmt.Printf("Projects available to %s:", who) 177 fmt.Println() 178 const max = 20 179 projs, err = projects(max) 180 for _, proj := range projs { 181 fmt.Println(" *", proj) 182 } 183 if err != nil { 184 return "", fmt.Errorf("list projects: %w", err) 185 } 186 if len(projs) == 0 { 187 fmt.Println("Create a project at https://console.cloud.google.com/") 188 return "", errors.New("no projects") 189 } 190 if len(projs) == max { 191 fmt.Println(" ... Wow, that is a lot of projects!") 192 fmt.Println("Type the name of any project, including ones not in this truncated list") 193 } 194 195 def, err := currentProject() 196 if err != nil { 197 return "", fmt.Errorf("get current project: %w", err) 198 } 199 200 choice = prompt("Select project", def) 201 202 // use default project 203 if choice == "" { 204 return def, nil 205 } 206 } 207 208 // is this a project from the list? 209 for _, p := range projs { 210 if p == choice { 211 return choice, nil 212 } 213 } 214 215 fmt.Printf("Ensuring %s has access to %s...", who, choice) 216 fmt.Println() 217 218 // no, make sure user has access to it 219 if err = exec.Command("gcloud", "projects", "describe", choice).Run(); err != nil { 220 return "", fmt.Errorf("%s cannot describe project: %w", who, err) 221 } 222 223 return choice, nil 224 } 225 226 // selectZone returns the user-selected zone, defaulting to the current gcloud one. 227 func selectZone() (string, error) { 228 const MaxZones = 20 229 230 def, err := currentZone() 231 if err != nil { 232 return "", fmt.Errorf("get current zone: %w", err) 233 } 234 235 fmt.Printf("Available zones:\n") 236 237 zoneList, err := zones() 238 if err != nil { 239 return "", fmt.Errorf("list zones: %w", err) 240 } 241 242 isNonEmpty := validateNotEmpty(zoneList) 243 if !isNonEmpty { 244 return "", errors.New("no available zones") 245 } 246 247 if def == "" { 248 // Arbitrarily select the first zone as the `default` if unset 249 def = zoneList[0] 250 } 251 252 isTruncated := printArray(zoneList, MaxZones) 253 if isTruncated { 254 fmt.Println(" ...") 255 fmt.Println("Type the name of any zone, including ones not in this truncated list") 256 } 257 258 choice := prompt("Select zone", def) 259 260 isContained := validateContainment(zoneList, choice) 261 if !isContained { 262 return "", fmt.Errorf("invalid zone selection: %s", choice) 263 } 264 265 return choice, nil 266 } 267 268 // cluster holds info about a GKE cluster 269 type cluster struct { 270 name string 271 zone string 272 project string 273 } 274 275 func (c cluster) context() string { 276 return fmt.Sprintf("gke_%s_%s_%s", c.project, c.zone, c.name) 277 } 278 279 // currentClusters returns a {name: cluster} map. 280 func currentClusters(proj string) (map[string]cluster, error) { 281 clusters, err := output("gcloud", "container", "clusters", "list", "--project="+proj, "--format=value(name,zone)") 282 if err != nil { 283 return nil, fmt.Errorf("list clusters: %w", err) 284 } 285 options := map[string]cluster{} 286 for _, line := range strings.Split(clusters, "\n") { 287 if len(line) == 0 { 288 continue 289 } 290 parts := strings.Split(line, "\t") 291 if len(parts) != 2 { 292 return nil, fmt.Errorf("bad line: %q", line) 293 } 294 c := cluster{name: parts[0], zone: parts[1], project: proj} 295 options[c.name] = c 296 } 297 return options, nil 298 } 299 300 // createCluster causes gcloud to create a cluster in project, returning the context name 301 func createCluster(proj, choice string) (*cluster, error) { 302 const def = "prow" 303 if choice == "" { 304 choice = prompt("Cluster name", def) 305 } 306 307 zone, err := selectZone() 308 if err != nil { 309 return nil, fmt.Errorf("select current zone for cluster: %w", err) 310 } 311 312 cmd := exec.Command("gcloud", "container", "clusters", "create", choice, "--zone="+zone, "--enable-legacy-authorization", "--issue-client-certificate") 313 cmd.Stdin = os.Stdin 314 cmd.Stdout = os.Stdout 315 cmd.Stderr = os.Stderr 316 if err := cmd.Run(); err != nil { 317 return nil, fmt.Errorf("create cluster: %w", err) 318 } 319 320 out, err := output("gcloud", "container", "clusters", "describe", choice, "--zone="+zone, "--format=value(name,zone)") 321 if err != nil { 322 return nil, fmt.Errorf("describe cluster: %w", err) 323 } 324 parts := strings.Split(out, "\t") 325 if len(parts) != 2 { 326 return nil, fmt.Errorf("bad describe cluster output: %s", out) 327 } 328 329 return &cluster{name: parts[0], zone: parts[1], project: proj}, nil 330 } 331 332 // createContext has the user create a context. 333 func createContext(co contextOptions) (string, error) { 334 proj, err := selectProject(co.project) 335 if err != nil { 336 logrus.Info("Run gcloud auth login to initialize gcloud") 337 return "", fmt.Errorf("get current project: %w", err) 338 } 339 340 fmt.Printf("Existing GKE clusters in %s:", proj) 341 fmt.Println() 342 clusters, err := currentClusters(proj) 343 if err != nil { 344 return "", fmt.Errorf("list %s clusters: %w", proj, err) 345 } 346 for name := range clusters { 347 fmt.Println(" *", name) 348 } 349 if len(clusters) == 0 { 350 fmt.Println(" No clusters") 351 } 352 var choice string 353 create := co.create 354 reuse := co.reuse 355 switch { 356 case create != "" && reuse != "": 357 return "", errors.New("Cannot use both --create and --reuse") 358 case create != "": 359 fmt.Println("Creating new " + create + " cluster...") 360 choice = "new" 361 case reuse != "": 362 fmt.Println("Reusing existing " + reuse + " cluster...") 363 choice = reuse 364 default: 365 choice = prompt("Get credentials for existing cluster or", "create new") 366 } 367 368 if choice == "" || choice == "new" || choice == "create new" { 369 cluster, err := createCluster(proj, create) 370 if err != nil { 371 return "", fmt.Errorf("create cluster in %s: %w", proj, err) 372 } 373 return cluster.context(), nil 374 } 375 376 cluster, ok := clusters[choice] 377 if !ok { 378 return "", fmt.Errorf("cluster not found: %s", choice) 379 } 380 cmd := exec.Command("gcloud", "container", "clusters", "get-credentials", cluster.name, "--project="+cluster.project, "--zone="+cluster.zone) 381 cmd.Stdin = os.Stdin 382 cmd.Stdout = os.Stdout 383 cmd.Stderr = os.Stderr 384 if err := cmd.Run(); err != nil { 385 return "", fmt.Errorf("get credentials: %w", err) 386 } 387 return cluster.context(), nil 388 } 389 390 // contextConfig returns the loader and config, which can create a clientconfig. 391 func contextConfig() (clientcmd.ClientConfigLoader, *clientcmdapi.Config, error) { 392 if err := ensureKubectl(); err != nil { 393 fmt.Println("Prow's tackler requires kubectl, please install:") 394 fmt.Println(" *", err) 395 if gerr := ensureGcloud(); gerr != nil { 396 fmt.Println(" *", gerr) 397 } 398 return nil, nil, errors.New("missing kubectl") 399 } 400 401 l := clientcmd.NewDefaultClientConfigLoadingRules() 402 c, err := l.Load() 403 return l, c, err 404 } 405 406 // selectContext allows the user to choose a context 407 // This may involve creating a cluster 408 func selectContext(co contextOptions) (string, error) { 409 fmt.Println("Existing kubernetes contexts:") 410 // get cluster context 411 _, cfg, err := contextConfig() 412 if err != nil { 413 logrus.WithError(err).Fatal("Failed to load ~/.kube/config from any obvious location") 414 } 415 // list contexts and ask to user to choose a context 416 options := map[int]string{} 417 418 var ctxs []string 419 for ctx := range cfg.Contexts { 420 ctxs = append(ctxs, ctx) 421 } 422 sort.Strings(ctxs) 423 for idx, ctx := range ctxs { 424 options[idx] = ctx 425 if ctx == cfg.CurrentContext { 426 fmt.Printf("* %d: %s (current)", idx, ctx) 427 } else { 428 fmt.Printf(" %d: %s", idx, ctx) 429 } 430 fmt.Println() 431 } 432 fmt.Println() 433 choice := co.context 434 switch { 435 case choice != "": 436 fmt.Println("Reuse " + choice + " context...") 437 case co.create != "" || co.reuse != "": 438 choice = "create" 439 fmt.Println("Create new context...") 440 default: 441 choice = prompt("Choose context or", "create new") 442 } 443 444 if choice == "create" || choice == "" || choice == "create new" || choice == "new" { 445 ctx, err := createContext(co) 446 if err != nil { 447 return "", fmt.Errorf("create context: %w", err) 448 } 449 return ctx, nil 450 } 451 452 if _, ok := cfg.Contexts[choice]; ok { 453 return choice, nil 454 } 455 456 idx, err := strconv.Atoi(choice) 457 if err != nil { 458 return "", fmt.Errorf("invalid context: %q", choice) 459 } 460 461 if ctx, ok := options[idx]; ok { 462 return ctx, nil 463 } 464 465 return "", fmt.Errorf("invalid index: %d", idx) 466 } 467 468 // applyCreate will dry-run create and then pipe this to kubectl apply. 469 // 470 // If we use the create verb it will fail if the secret already exists. 471 // And kubectl will reject the apply verb with a secret. 472 func applyCreate(ctx string, args ...string) error { 473 create := exec.Command("kubectl", append([]string{"--dry-run=true", "--output=yaml", "create"}, args...)...) 474 create.Stderr = os.Stderr 475 obj, err := create.StdoutPipe() 476 if err != nil { 477 return fmt.Errorf("rolebinding pipe: %w", err) 478 } 479 480 if err := create.Start(); err != nil { 481 return fmt.Errorf("start create: %w", err) 482 } 483 if err := apply(ctx, obj); err != nil { 484 return fmt.Errorf("apply: %w", err) 485 } 486 if err := create.Wait(); err != nil { 487 return fmt.Errorf("create: %w", err) 488 } 489 return nil 490 } 491 492 func apply(ctx string, in io.Reader) error { 493 apply := exec.Command("kubectl", "--context="+ctx, "apply", "-f", "-") 494 apply.Stderr = os.Stderr 495 apply.Stdout = os.Stdout 496 apply.Stdin = in 497 if err := apply.Start(); err != nil { 498 return fmt.Errorf("start: %w", err) 499 } 500 return apply.Wait() 501 } 502 503 func applyRoleBinding(context string) error { 504 who, err := currentAccount() 505 if err != nil { 506 return fmt.Errorf("current account: %w", err) 507 } 508 return applyCreate(context, "clusterrolebinding", "prow-admin", "--clusterrole=cluster-admin", "--user="+who) 509 } 510 511 type options struct { 512 githubTokenPath string 513 starter string 514 repos flagutil.Strings 515 contextOptions 516 confirm bool 517 } 518 519 type contextOptions struct { 520 context string 521 create string 522 reuse string 523 project string 524 } 525 526 func addFlags(fs *flag.FlagSet) *options { 527 var o options 528 fs.StringVar(&o.githubTokenPath, "github-token-path", "", "Path to github token") 529 fs.StringVar(&o.starter, "starter", "", "Apply starter.yaml from the following path or URL (use upstream for latest)") 530 fs.Var(&o.repos, "repo", "Send prow webhooks for these orgs or org/repos (repeat as necessary)") 531 fs.StringVar(&o.context, "context", "", "Choose kubeconfig context to use") 532 fs.StringVar(&o.create, "create", "", "name of cluster to create in --project") 533 fs.StringVar(&o.reuse, "reuse", "", "Reuse existing cluster in --project") 534 fs.StringVar(&o.project, "project", "", "GCP project to get/create cluster") 535 fs.BoolVar(&o.confirm, "confirm", false, "Overwrite existing prow deployments without asking if set") 536 return &o 537 } 538 539 func githubToken(choice string) (string, error) { 540 if choice == "" { 541 fmt.Print("Store your GitHub token in a file e.g. echo $TOKEN > /path/to/github/token\n") 542 choice = prompt("Input /path/to/github/token to upload into cluster", "") 543 } 544 path := os.ExpandEnv(choice) 545 if _, err := os.Stat(path); err != nil { 546 return "", fmt.Errorf("open %s: %w", path, err) 547 } 548 return path, nil 549 } 550 551 func githubClient(tokenPath string, dry bool) (github.Client, error) { 552 if err := secret.Add(tokenPath); err != nil { 553 return nil, fmt.Errorf("start agent: %w", err) 554 } 555 556 gen := secret.GetTokenGenerator(tokenPath) 557 censor := secret.Censor 558 if dry { 559 return github.NewDryRunClient(gen, censor, github.DefaultGraphQLEndpoint, github.DefaultAPIEndpoint) 560 } 561 return github.NewClient(gen, censor, github.DefaultGraphQLEndpoint, github.DefaultAPIEndpoint) 562 } 563 564 func applySecret(ctx, ns, name, key, path string) error { 565 return applyCreate(ctx, "secret", "generic", name, "--from-file="+key+"="+path, "--namespace="+ns) 566 } 567 568 func applyStarter(kc *kubernetes.Clientset, ns, choice, ctx string, overwrite bool) error { 569 const defaultStarter = "https://raw.githubusercontent.com/kubernetes/test-infra/master/config/prow/cluster/starter/starter-gcs.yaml" 570 571 if choice == "" { 572 choice = prompt("Apply starter.yaml from", "github upstream") 573 } 574 if choice == "" || choice == "github" || choice == "upstream" || choice == "github upstream" { 575 choice = defaultStarter 576 fmt.Println("Loading from", choice) 577 } 578 _, err := kc.AppsV1().Deployments(ns).Get(context2.TODO(), "plank", metav1.GetOptions{}) 579 switch { 580 case err != nil && apierrors.IsNotFound(err): 581 // Great, new clean namespace to deploy! 582 case err != nil: // unexpected error 583 return fmt.Errorf("get plank: %w", err) 584 case !overwrite: // already a plank, confirm overwrite 585 overwriteChoice := prompt(fmt.Sprintf("Prow is already deployed to %s in %s, overwrite?", ns, ctx), "no") 586 switch overwriteChoice { 587 case "y", "Y", "yes": 588 // carry on, then 589 default: 590 return errors.New("prow already deployed") 591 } 592 } 593 apply := exec.Command("kubectl", "--context="+ctx, "apply", "-f", choice) 594 apply.Stderr = os.Stderr 595 apply.Stdout = os.Stdout 596 return apply.Run() 597 } 598 599 func clientConfigNamespace(context string) (string, bool, error) { 600 loader, cfg, err := contextConfig() 601 if err != nil { 602 return "", false, fmt.Errorf("load contexts: %w", err) 603 } 604 605 return clientcmd.NewNonInteractiveClientConfig(*cfg, context, &clientcmd.ConfigOverrides{}, loader).Namespace() 606 } 607 608 func clientConfig(context string) (*rest.Config, error) { 609 loader, cfg, err := contextConfig() 610 if err != nil { 611 return nil, fmt.Errorf("load contexts: %w", err) 612 } 613 614 return clientcmd.NewNonInteractiveClientConfig(*cfg, context, &clientcmd.ConfigOverrides{}, loader).ClientConfig() 615 } 616 617 func ingress(kc *kubernetes.Clientset, ns, service string) (url.URL, error) { 618 for { 619 var ing *networking.IngressList 620 var err error 621 622 // Detect ingress API to use based on Kubernetes version 623 ing, err = kc.NetworkingV1().Ingresses(ns).List(context2.TODO(), metav1.ListOptions{}) 624 625 if err != nil { 626 logrus.WithError(err).Fatalf("Could not get ingresses for service: %s", service) 627 } 628 629 var best url.URL 630 points := 0 631 for _, ing := range ing.Items { 632 // does this ingress route to the hook service? 633 cur := -1 634 var maybe url.URL 635 for _, r := range ing.Spec.Rules { 636 h := r.IngressRuleValue.HTTP 637 if h == nil { 638 continue 639 } 640 for _, p := range h.Paths { 641 if p.Backend.Service.Name != service { 642 continue 643 } 644 maybe.Scheme = "http" 645 maybe.Host = r.Host 646 maybe.Path = p.Path 647 cur = 0 648 break 649 } 650 } 651 if cur < 0 { 652 continue // no 653 } 654 655 // does it have an ip or hostname? 656 for _, tls := range ing.Spec.TLS { 657 for _, h := range tls.Hosts { 658 if h == maybe.Host { 659 cur = 3 660 maybe.Scheme = "https" 661 break 662 } 663 } 664 } 665 666 if cur == 0 { 667 for _, i := range ing.Status.LoadBalancer.Ingress { 668 if maybe.Host != "" && maybe.Host == i.Hostname { 669 cur = 2 670 break 671 } 672 if i.IP != "" { 673 cur = 1 674 if maybe.Host == "" { 675 maybe.Host = i.IP 676 } 677 break 678 } 679 } 680 } 681 if cur > points { 682 best = maybe 683 points = cur 684 } 685 } 686 if points > 0 { 687 return best, nil 688 } 689 fmt.Print(".") 690 time.Sleep(1 * time.Second) 691 } 692 } 693 694 func hmacSecret() string { 695 buf := make([]byte, 20) 696 rand.Read(buf) 697 return fmt.Sprintf("%x", buf) 698 } 699 700 func findHook(client github.Client, org, repo string, loc url.URL) (*github.Hook, error) { 701 loc.Scheme = "" 702 goal := loc.String() 703 var hooks []github.Hook 704 var err error 705 if repo == "" { 706 hooks, err = client.ListOrgHooks(org) 707 } else { 708 hooks, err = client.ListRepoHooks(org, repo) 709 } 710 if err != nil { 711 return nil, fmt.Errorf("list hooks: %w", err) 712 } 713 714 for _, h := range hooks { 715 u, err := url.Parse(h.Config.URL) 716 if err != nil { 717 logrus.WithError(err).Warnf("Invalid %s/%s hook url %s", org, repo, h.Config.URL) 718 continue 719 } 720 u.Scheme = "" 721 if u.String() == goal { 722 return &h, nil 723 } 724 } 725 return nil, nil 726 } 727 728 func orgRepo(in string) (string, string) { 729 parts := strings.SplitN(in, "/", 2) 730 org := parts[0] 731 var repo string 732 if len(parts) == 2 { 733 repo = parts[1] 734 } 735 return org, repo 736 } 737 738 func ensureHmac(kc *kubernetes.Clientset, ns string) (string, error) { 739 secret, err := kc.CoreV1().Secrets(ns).Get(context2.TODO(), "hmac-token", metav1.GetOptions{}) 740 if err != nil && !apierrors.IsNotFound(err) { 741 return "", fmt.Errorf("get: %w", err) 742 } 743 if err == nil { 744 buf, ok := secret.Data["hmac"] 745 if ok { 746 return string(buf), nil 747 } 748 logrus.Warn("hmac-token secret does not contain an hmac key, replacing secret with new random data...") 749 } else { 750 logrus.Info("Creating new hmac-token secret with random data...") 751 } 752 hmac := hmacSecret() 753 secret = &corev1.Secret{} 754 secret.Name = "hmac-token" 755 secret.Namespace = ns 756 secret.StringData = map[string]string{"hmac": hmac} 757 if err == nil { 758 if _, err = kc.CoreV1().Secrets(ns).Update(context2.TODO(), secret, metav1.UpdateOptions{}); err != nil { 759 return "", fmt.Errorf("update: %w", err) 760 } 761 } else { 762 if _, err = kc.CoreV1().Secrets(ns).Create(context2.TODO(), secret, metav1.CreateOptions{}); err != nil { 763 return "", fmt.Errorf("create: %w", err) 764 } 765 } 766 return hmac, nil 767 } 768 769 func enableHooks(client github.Client, loc url.URL, secret string, repos ...string) ([]string, error) { 770 var enabled []string 771 locStr := loc.String() 772 hasFlagValues := len(repos) > 0 773 for { 774 var choice string 775 switch { 776 case !hasFlagValues: 777 if len(enabled) > 0 { 778 fmt.Println("Enabled so far:", strings.Join(enabled, ", ")) 779 } 780 choice = prompt("Enable which org or org/repo", "quit") 781 case len(repos) > 0: 782 choice = repos[0] 783 repos = repos[1:] 784 default: 785 choice = "" 786 } 787 if choice == "" || choice == "quit" { 788 return enabled, nil 789 } 790 org, repo := orgRepo(choice) 791 hook, err := findHook(client, org, repo, loc) 792 if err != nil { 793 return enabled, fmt.Errorf("find %s hook in %s: %w", locStr, choice, err) 794 } 795 yes := true 796 j := "json" 797 req := github.HookRequest{ 798 Name: "web", 799 Active: &yes, 800 Events: github.AllHookEvents, 801 Config: &github.HookConfig{ 802 URL: locStr, 803 ContentType: &j, 804 Secret: &secret, 805 }, 806 } 807 if hook == nil { 808 var id int 809 if repo == "" { 810 id, err = client.CreateOrgHook(org, req) 811 } else { 812 id, err = client.CreateRepoHook(org, repo, req) 813 } 814 if err != nil { 815 return enabled, fmt.Errorf("create %s hook in %s: %w", locStr, choice, err) 816 } 817 fmt.Printf("Created hook %d to %s in %s", id, locStr, choice) 818 fmt.Println() 819 } else { 820 if repo == "" { 821 err = client.EditOrgHook(org, hook.ID, req) 822 } else { 823 err = client.EditRepoHook(org, repo, hook.ID, req) 824 } 825 if err != nil { 826 return enabled, fmt.Errorf("edit %s hook %d in %s: %w", locStr, hook.ID, choice, err) 827 } 828 } 829 enabled = append(enabled, choice) 830 } 831 } 832 833 func ensureConfigMap(kc *kubernetes.Clientset, ns, name, key string) error { 834 cm, err := kc.CoreV1().ConfigMaps(ns).Get(context2.TODO(), name, metav1.GetOptions{}) 835 if err != nil { 836 if !apierrors.IsNotFound(err) { 837 return fmt.Errorf("get: %w", err) 838 } 839 cm = &corev1.ConfigMap{ 840 Data: map[string]string{key: ""}, 841 } 842 cm.Name = name 843 cm.Namespace = ns 844 _, err := kc.CoreV1().ConfigMaps(ns).Create(context2.TODO(), cm, metav1.CreateOptions{}) 845 if err != nil { 846 return fmt.Errorf("create: %w", err) 847 } 848 } 849 850 if _, ok := cm.Data[key]; ok { 851 return nil 852 } 853 logrus.Warnf("%s/%s missing key %s, adding...", ns, name, key) 854 if cm.Data == nil { 855 cm.Data = map[string]string{} 856 } 857 cm.Data[key] = "" 858 if _, err := kc.CoreV1().ConfigMaps(ns).Update(context2.TODO(), cm, metav1.UpdateOptions{}); err != nil { 859 return fmt.Errorf("update: %w", err) 860 } 861 return nil 862 } 863 864 func main() { 865 fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 866 skipGitHub := fs.Bool("skip-github", false, "Do not add github webhooks if set") 867 opt := addFlags(fs) 868 fs.Parse(os.Args[1:]) 869 870 const ns = "prow" 871 872 ctx, err := selectContext(opt.contextOptions) 873 if err != nil { 874 logrus.WithError(err).Fatal("Failed to select context") 875 } 876 877 ctxNamespace, _, err := clientConfigNamespace(ctx) 878 if err != nil { 879 logrus.WithError(err).Fatal("Failed to reload ~/.kube/config from any obvious location") 880 } 881 882 if ctxNamespace != ns { 883 logrus.Warnf("Context %s specifies namespace %s, but Prow resources will be installed in namespace %s.", ctx, ctxNamespace, ns) 884 } 885 886 // get kubernetes client 887 clientCfg, err := clientConfig(ctx) 888 if err != nil { 889 logrus.WithError(err).Fatal("Failed to reload ~/.kube/config from any obvious location") 890 } 891 892 kc, err := kubernetes.NewForConfig(clientCfg) 893 if err != nil { 894 logrus.WithError(err).Fatal("Failed to create kubernetes client") 895 } 896 897 fmt.Println("Applying admin role bindings (to create RBAC rules)...") 898 if err := applyRoleBinding(ctx); err != nil { 899 logrus.WithError(err).Fatalf("Failed to apply cluster role binding to %s", ctx) 900 } 901 902 fmt.Println("Deploying prow...") 903 if err := applyStarter(kc, ns, opt.starter, ctx, opt.confirm); err != nil { 904 logrus.WithError(err).Fatal("Could not deploy prow") 905 } 906 907 // configure plugins.yaml and config.yaml 908 // TODO(fejta): throw up an editor 909 if err = ensureConfigMap(kc, ns, "config", "config.yaml"); err != nil { 910 logrus.WithError(err).Fatal("Failed to ensure config.yaml exists") 911 } 912 if err = ensureConfigMap(kc, ns, "plugins", "plugins.yaml"); err != nil { 913 logrus.WithError(err).Fatal("Failed to ensure plugins.yaml exists") 914 } 915 916 if !*skipGitHub { 917 fmt.Println("Checking github credentials...") 918 // create github client 919 token, err := githubToken(opt.githubTokenPath) 920 if err != nil { 921 logrus.WithError(err).Fatal("Failed to get github token") 922 } 923 client, err := githubClient(token, false) 924 if err != nil { 925 logrus.WithError(err).Fatal("Failed to create github client") 926 } 927 who, err := client.BotUser() 928 if err != nil { 929 logrus.WithError(err).Fatal("Cannot access github account name") 930 } 931 fmt.Println("Prow will act as", who.Login, "on github") 932 933 // create github secrets 934 fmt.Print("Applying github token into oauth-token secret...") 935 if err := applySecret(ctx, ns, "oauth-token", "oauth", token); err != nil { 936 logrus.WithError(err).Fatal("Could not apply github oauth token secret") 937 } 938 939 fmt.Print("Ensuring hmac secret exists at hmac-token...") 940 hmac, err := ensureHmac(kc, ns) 941 if err != nil { 942 logrus.WithError(err).Fatal("Failed to ensure hmac-token exists") 943 } 944 fmt.Println("exists") 945 946 fmt.Print("Looking for prow's hook ingress URL... ") 947 url, err := ingress(kc, ns, "hook") 948 if err != nil { 949 logrus.WithError(err).Fatal("Could not determine webhook ingress URL") 950 } 951 fmt.Println(url.String()) 952 953 // TODO(fejta): ensure plugins are enabled for all these repos 954 _, err = enableHooks(client, url, hmac, opt.repos.Strings()...) 955 if err != nil { 956 logrus.WithError(err).Fatalf("Could not configure repos to send %s webhooks.", url.String()) 957 } 958 } 959 960 deck, err := ingress(kc, ns, "deck") 961 if err != nil { 962 logrus.WithError(err).Fatalf("Could not find deck URL") 963 } 964 deck.Path = strings.TrimRight(deck.Path, "*") 965 fmt.Printf("Enjoy your %s prow instance at: %s", ctx, deck.String()) 966 fmt.Println() 967 }