github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/mkbuild-cluster/main.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"os"
    25  	"os/exec"
    26  	"strings"
    27  
    28  	"github.com/sirupsen/logrus"
    29  	"sigs.k8s.io/yaml"
    30  
    31  	"k8s.io/test-infra/prow/kube"
    32  )
    33  
    34  const (
    35  	useClientCertEnv = "CLOUDSDK_CONTAINER_USE_CLIENT_CERTIFICATE"
    36  )
    37  
    38  type options struct {
    39  	account       string
    40  	alias         string
    41  	changeContext bool
    42  	cluster       string
    43  	getClientCert bool
    44  	overwrite     bool
    45  	printEntry    bool
    46  	printData     bool
    47  	project       string
    48  	skipCheck     bool
    49  	zone          string
    50  }
    51  
    52  type describe struct {
    53  	Auth     describeAuth `json:"masterAuth"`
    54  	Endpoint string       `json:"endpoint"`
    55  }
    56  
    57  type describeAuth struct {
    58  	ClientCertificate    []byte `json:"clientCertificate"`
    59  	ClientKey            []byte `json:"clientKey"`
    60  	ClusterCACertificate []byte `json:"clusterCaCertificate"`
    61  }
    62  
    63  func parseOptions() options {
    64  	var o options
    65  	if err := o.parseArgs(flag.CommandLine, os.Args[1:]); err != nil {
    66  		logrus.Fatalf("Invalid flags: %v", err)
    67  	}
    68  	return o
    69  }
    70  
    71  func (o *options) parseArgs(flags *flag.FlagSet, args []string) error {
    72  	flags.StringVar(&o.account, "account", "", "use this account to describe --cluster")
    73  	flags.StringVar(&o.alias, "alias", "", "the --build-cluster alias to add")
    74  	flags.StringVar(&o.cluster, "cluster", "", "the GKE cluster to describe")
    75  	flags.StringVar(&o.project, "project", "", "the GKE project to describe")
    76  	flags.StringVar(&o.zone, "zone", "", "the GKE zone to describe")
    77  	flags.BoolVar(&o.printData, "print-file", false, "print the file outside of the configmap secret")
    78  	flags.BoolVar(&o.printEntry, "print-entry", false, "print the new entry without appending to existing ones at stdin")
    79  	flags.BoolVar(&o.getClientCert, "get-client-cert", false, fmt.Sprintf("first get-credentials for the cluster using %s=True", useClientCertEnv))
    80  	flags.BoolVar(&o.changeContext, "change-context", false, "allow --get-client-cert to change kubectl config current-context")
    81  	flags.BoolVar(&o.skipCheck, "skip-check", false, "skip validating the creds work in a client")
    82  	switch err := flags.Parse(args); {
    83  	case err != nil:
    84  		return err
    85  	case o.cluster == "":
    86  		return errors.New("--cluster required")
    87  	case o.project == "":
    88  		return errors.New("--project required")
    89  	case o.zone == "":
    90  		return errors.New("--zone required")
    91  	case o.alias == "":
    92  		return fmt.Errorf("--alias required (use %q for default)", kube.DefaultClusterAlias)
    93  	}
    94  	return nil
    95  }
    96  
    97  func main() {
    98  	// Gather options from flags
    99  	o := parseOptions()
   100  	if err := do(o); err != nil {
   101  		logrus.Fatalf("Failed: %v", err)
   102  	}
   103  }
   104  
   105  // useContext calls kubectl config use-context ctx
   106  func useContext(o options, ctx string) error {
   107  	_, cmd := command("kubectl", "config", "use-context", ctx)
   108  	return cmd.Run()
   109  }
   110  
   111  // currentContext returns kubectl config current-context
   112  func currentContext(o options) (string, error) {
   113  	_, cmd := command("kubectl", "config", "current-context")
   114  	b, err := cmd.Output()
   115  	return strings.TrimSpace(string(b)), err
   116  }
   117  
   118  // getCredentials calls gcloud container clusters get-credentials, usually preserving currentContext()
   119  func getCredentials(o options) error {
   120  	if !o.changeContext {
   121  		cur, err := currentContext(o)
   122  		if err != nil {
   123  			return fmt.Errorf("read current-context: %v", err)
   124  		}
   125  		defer useContext(o, cur)
   126  	}
   127  
   128  	// TODO(fejta): we ought to update kube.Client to support modern auth methods.
   129  	// More info: https://github.com/kubernetes/kubernetes/issues/30617
   130  	old, set := os.LookupEnv(useClientCertEnv)
   131  	if set {
   132  		defer os.Setenv(useClientCertEnv, old)
   133  	}
   134  	if err := os.Setenv("CLOUDSDK_CONTAINER_USE_CLIENT_CERTIFICATE", "True"); err != nil {
   135  		return fmt.Errorf("failed to set %s: %v", useClientCertEnv, err)
   136  	}
   137  	args, cmd := command(
   138  		"gcloud", "container", "clusters", "get-credentials", o.cluster,
   139  		"--project", o.project,
   140  		"--zone", o.zone,
   141  	)
   142  	if err := cmd.Run(); err != nil {
   143  		return fmt.Errorf("%s: %v", strings.Join(args, " "), err)
   144  	}
   145  	return nil
   146  }
   147  
   148  // command creates an exec.Cmd with Stderr piped to os.Stderr and returns the args
   149  func command(bin string, args ...string) ([]string, *exec.Cmd) {
   150  	cmd := exec.Command(bin, args...)
   151  	cmd.Stderr = os.Stderr
   152  	return append([]string{bin}, args...), cmd
   153  }
   154  
   155  // getAccount returns gcloud config get-value core/account
   156  func getAccount() (string, error) {
   157  	args, cmd := command("gcloud", "config", "get-value", "core/account")
   158  	b, err := cmd.Output()
   159  	if err != nil {
   160  		return "", fmt.Errorf("%s: %v", strings.Join(args, " "), err)
   161  	}
   162  	return strings.TrimSpace(string(b)), nil
   163  }
   164  
   165  // setAccount calls gcloud config set core/account
   166  func setAccount(account string) error {
   167  	_, cmd := command("gcloud", "config", "set", "core/account", account)
   168  	return cmd.Run()
   169  }
   170  
   171  // describeCluster returns details from gcloud container clusters describe.
   172  func describeCluster(o options) (*describe, error) {
   173  	if o.account != "" {
   174  		act, err := getAccount()
   175  		if err != nil {
   176  			return nil, fmt.Errorf("get current account: %v", err)
   177  		}
   178  		defer setAccount(act)
   179  		if err = setAccount(o.account); err != nil {
   180  			return nil, fmt.Errorf("set account %s: %v", o.account, err)
   181  		}
   182  	}
   183  	args, cmd := command(
   184  		"gcloud", "container", "clusters", "describe", o.cluster,
   185  		"--project", o.project,
   186  		"--zone", o.zone,
   187  		"--format=yaml",
   188  	)
   189  	data, err := cmd.Output()
   190  	if err != nil {
   191  		return nil, fmt.Errorf("%s: %v", strings.Join(args, " "), err)
   192  	}
   193  	var d describe
   194  	if yaml.Unmarshal(data, &d); err != nil {
   195  		return nil, fmt.Errorf("unmarshal gcloud: %v", err)
   196  	}
   197  
   198  	if d.Endpoint == "" {
   199  		return nil, errors.New("empty endpoint")
   200  	}
   201  	if len(d.Auth.ClusterCACertificate) == 0 {
   202  		return nil, errors.New("empty clusterCaCertificate")
   203  	}
   204  
   205  	if len(d.Auth.ClientKey) == 0 {
   206  		return nil, errors.New("empty clientKey, consider running with --get-client-cert")
   207  	}
   208  	if len(d.Auth.ClientCertificate) == 0 {
   209  		return nil, errors.New("empty clientCertificate, consider running with --get-client-cert")
   210  	}
   211  
   212  	return &d, nil
   213  }
   214  
   215  // do will get creds for the specified cluster and add them to the stdin secret
   216  func do(o options) error {
   217  	// Refresh credentials if requested
   218  	if o.getClientCert {
   219  		if err := getCredentials(o); err != nil {
   220  			return fmt.Errorf("get client cert: %v", err)
   221  		}
   222  	}
   223  	// Create the new cluster entry
   224  	d, err := describeCluster(o)
   225  	if err != nil {
   226  		return fmt.Errorf("describe auth: %v", err)
   227  	}
   228  	newCluster := kube.Cluster{
   229  		Endpoint:             "https://" + d.Endpoint,
   230  		ClusterCACertificate: d.Auth.ClusterCACertificate,
   231  		ClientKey:            d.Auth.ClientKey,
   232  		ClientCertificate:    d.Auth.ClientCertificate,
   233  	}
   234  
   235  	// Try to use this entry
   236  	if !o.skipCheck {
   237  		c, err := kube.NewClient(&newCluster, "kube-system")
   238  		if err != nil {
   239  			return err
   240  		}
   241  		if _, err = c.ListPods("k8s-app=kube-dns"); err != nil {
   242  			return fmt.Errorf("authenticated client could not list pods: %v", err)
   243  		}
   244  	}
   245  
   246  	// Just print this entry if requested
   247  	if o.printEntry {
   248  		data, err := kube.MarshalClusterMap(map[string]kube.Cluster{o.alias: newCluster})
   249  		if err != nil {
   250  			return fmt.Errorf("marshal %s: %v", o.alias, err)
   251  		}
   252  		fmt.Println(string(data))
   253  		return nil
   254  	}
   255  
   256  	// Append the new entry to the current secret
   257  
   258  	// First read in the secret from stdin
   259  	b, err := ioutil.ReadAll(os.Stdin)
   260  	if err != nil {
   261  		return fmt.Errorf("read stdin: %v", err)
   262  	}
   263  	var s kube.Secret
   264  	if err := yaml.Unmarshal(b, &s); err != nil {
   265  		return fmt.Errorf("unmarshal stdin: %v", err)
   266  	}
   267  
   268  	// Now decode the {alias: cluster} map and print out current keys
   269  	clusters, err := kube.UnmarshalClusterMap(s.Data["cluster"])
   270  	if err != nil {
   271  		return fmt.Errorf("unmarshal secret: %v", err)
   272  	}
   273  	var existing []string
   274  	for a := range clusters {
   275  		existing = append(existing, a)
   276  	}
   277  	logrus.Infof("Existing clusters: %s", strings.Join(existing, ", "))
   278  
   279  	// Add new key
   280  	_, ok := clusters[o.alias]
   281  	if ok && !o.overwrite {
   282  		return fmt.Errorf("cluster %s already exists", o.alias)
   283  	}
   284  	clusters[o.alias] = newCluster
   285  	logrus.Infof("New cluster: %s", o.alias)
   286  
   287  	// Marshal the {alias: cluster} map back into secret data
   288  	data, err := kube.MarshalClusterMap(clusters)
   289  	if err != nil {
   290  		return fmt.Errorf("marshal clusters: %v", err)
   291  	}
   292  
   293  	if o.printData { // Just print the data outside of the secret
   294  		fmt.Println(string(data))
   295  		return nil
   296  	}
   297  
   298  	// Output the new secret
   299  	s.Data["cluster"] = data
   300  	buf, err := yaml.Marshal(s)
   301  	if err != nil {
   302  		return fmt.Errorf("marshal secret: %v", err)
   303  	}
   304  	fmt.Println(string(buf))
   305  	return nil
   306  }