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