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