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  }