github.com/jenkins-x/jx/v2@v2.1.155/pkg/cloud/gke/gcloud.go (about)

     1  package gke
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	osUser "os/user"
    16  
    17  	"github.com/jenkins-x/jx-logging/pkg/log"
    18  	"github.com/jenkins-x/jx/v2/pkg/kube/naming"
    19  	"github.com/jenkins-x/jx/v2/pkg/util"
    20  	"github.com/pkg/errors"
    21  	v1 "k8s.io/api/core/v1"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/client-go/kubernetes"
    24  	"sigs.k8s.io/yaml"
    25  )
    26  
    27  // KmsLocation indicates the location used by the Google KMS service
    28  const KmsLocation = "global"
    29  
    30  var (
    31  	// RequiredServiceAccountRoles the roles required to create a cluster with terraform
    32  	RequiredServiceAccountRoles = []string{"roles/owner"}
    33  
    34  	// KanikoServiceAccountRoles the roles required to run kaniko with GCS
    35  	KanikoServiceAccountRoles = []string{"roles/storage.admin",
    36  		"roles/storage.objectAdmin",
    37  		"roles/storage.objectCreator"}
    38  
    39  	// VeleroServiceAccountRoles the roles required to run velero with GCS
    40  	VeleroServiceAccountRoles = []string{
    41  		/* TODO
    42  		"roles/compute.disks.get",
    43  		"roles/compute.disks.create",
    44  		"roles/compute.disks.createSnapshot",
    45  		"roles/compute.snapshots.get",
    46  		"roles/compute.snapshots.create",
    47  		"roles/compute.snapshots.useReadOnly",
    48  		"roles/compute.snapshots.delete",
    49  		"roles/compute.zones.get",
    50  		*/
    51  		"roles/storage.admin",
    52  		"roles/storage.objectAdmin",
    53  		"roles/storage.objectCreator"}
    54  )
    55  
    56  // GCloud real implementation of the gcloud helper
    57  type GCloud struct {
    58  }
    59  
    60  // Cluster struct to represent a cluster on gcloud
    61  type Cluster struct {
    62  	Name           string            `json:"name,omitempty"`
    63  	ResourceLabels map[string]string `json:"resourceLabels,omitempty"`
    64  	Status         string            `json:"status,omitempty"`
    65  	Location       string            `json:"location,omitempty"`
    66  }
    67  
    68  type recordSet struct {
    69  	Kind    string   `json:"kind"`
    70  	Name    string   `json:"name"`
    71  	Rrdatas []string `json:"rrdatas"`
    72  	TTL     int      `json:"ttl"`
    73  	Type    string   `json:"type"`
    74  }
    75  
    76  // generateManagedZoneName constructs and returns a managed zone name using the domain value
    77  func generateManagedZoneName(domain string) string {
    78  
    79  	var managedZoneName string
    80  
    81  	if domain != "" {
    82  		managedZoneName = strings.Replace(domain, ".", "-", -1)
    83  		return fmt.Sprintf("%s-zone", managedZoneName)
    84  	}
    85  
    86  	return ""
    87  
    88  }
    89  
    90  // getManagedZoneName checks for a given domain zone within the specified project and returns its name
    91  func getManagedZoneName(projectID string, domain string) (string, error) {
    92  	args := []string{"dns",
    93  		"managed-zones",
    94  		fmt.Sprintf("--project=%s", projectID),
    95  		"list",
    96  		fmt.Sprintf("--filter=%s.", domain),
    97  		"--format=json",
    98  	}
    99  
   100  	cmd := util.Command{
   101  		Name: "gcloud",
   102  		Args: args,
   103  	}
   104  
   105  	output, err := cmd.RunWithoutRetry()
   106  	if err != nil {
   107  		return "", errors.Wrap(err, "executing gcloud dns managed-zones list command ")
   108  	}
   109  
   110  	type managedZone struct {
   111  		Name string `json:"name"`
   112  	}
   113  
   114  	var managedZones []managedZone
   115  
   116  	err = yaml.Unmarshal([]byte(output), &managedZones)
   117  	if err != nil {
   118  		return "", errors.Wrap(err, "unmarshalling gcloud response")
   119  	}
   120  
   121  	if len(managedZones) == 1 {
   122  		return managedZones[0].Name, nil
   123  	}
   124  
   125  	return "", nil
   126  }
   127  
   128  // CreateManagedZone creates a managed zone for the given domain in the specified project
   129  func (g *GCloud) CreateManagedZone(projectID string, domain string) error {
   130  	managedZoneName, err := getManagedZoneName(projectID, domain)
   131  	if err != nil {
   132  		return errors.Wrap(err, "unable to determine whether managed zone exists")
   133  	}
   134  	if managedZoneName == "" {
   135  		log.Logger().Infof("Managed Zone doesn't exist for %s domain, creating...", domain)
   136  		managedZoneName := generateManagedZoneName(domain)
   137  		args := []string{"dns",
   138  			"managed-zones",
   139  			fmt.Sprintf("--project=%s", projectID),
   140  			"create",
   141  			managedZoneName,
   142  			"--dns-name",
   143  			fmt.Sprintf("%s.", domain),
   144  			"--description=managed-zone utilised by jx",
   145  		}
   146  
   147  		cmd := util.Command{
   148  			Name: "gcloud",
   149  			Args: args,
   150  		}
   151  
   152  		_, err := cmd.RunWithoutRetry()
   153  		if err != nil {
   154  			return errors.Wrap(err, "executing gcloud dns managed-zones list command ")
   155  		}
   156  	} else {
   157  		log.Logger().Infof("Managed Zone exists for %s domain.", domain)
   158  	}
   159  	return nil
   160  }
   161  
   162  // CreateDNSZone creates the DNS zone if it doesn't exist
   163  // and returns the list of name servers for the given domain and project
   164  func (g *GCloud) CreateDNSZone(projectID string, domain string) (string, []string, error) {
   165  	var managedZone, nameServers = "", []string{}
   166  	err := g.CreateManagedZone(projectID, domain)
   167  	if err != nil {
   168  		return "", []string{}, errors.Wrap(err, "while trying to creating a CloudDNS managed zone")
   169  	}
   170  	managedZone, nameServers, err = g.GetManagedZoneNameServers(projectID, domain)
   171  	if err != nil {
   172  		return "", []string{}, errors.Wrap(err, "while trying to retrieve the managed zone name servers")
   173  	}
   174  
   175  	// Update TTL to 60 for managed zone NS record
   176  	err = updateManagedZoneRecordTTL(projectID, domain, managedZone, "NS", 60)
   177  	if err != nil {
   178  		return "", []string{}, errors.Wrap(err, "when trying to update the managed zone NS record-set")
   179  	}
   180  
   181  	// Update TTL to 60 for managed zone SOA record
   182  	err = updateManagedZoneRecordTTL(projectID, domain, managedZone, "SOA", 60)
   183  	if err != nil {
   184  		return "", []string{}, errors.Wrap(err, "when trying to update the managed zone SOA record-set")
   185  	}
   186  
   187  	return managedZone, nameServers, nil
   188  }
   189  
   190  // GetManagedZoneNameServers retrieves a list of name servers associated with a zone
   191  func (g *GCloud) GetManagedZoneNameServers(projectID string, domain string) (string, []string, error) {
   192  	var nameServers = []string{}
   193  	managedZoneName, err := getManagedZoneName(projectID, domain)
   194  	if err != nil {
   195  		return "", []string{}, errors.Wrap(err, "unable to determine whether managed zone exists")
   196  	}
   197  	if managedZoneName != "" {
   198  		log.Logger().Infof("Getting nameservers for %s domain", domain)
   199  		args := []string{"dns",
   200  			"managed-zones",
   201  			fmt.Sprintf("--project=%s", projectID),
   202  			"describe",
   203  			managedZoneName,
   204  			"--format=json",
   205  		}
   206  
   207  		cmd := util.Command{
   208  			Name: "gcloud",
   209  			Args: args,
   210  		}
   211  
   212  		type mz struct {
   213  			Name        string   `json:"name"`
   214  			NameServers []string `json:"nameServers"`
   215  		}
   216  
   217  		var managedZone mz
   218  
   219  		output, err := cmd.RunWithoutRetry()
   220  		if err != nil {
   221  			return "", []string{}, errors.Wrap(err, "executing gcloud dns managed-zones list command ")
   222  		}
   223  
   224  		err = json.Unmarshal([]byte(output), &managedZone)
   225  		if err != nil {
   226  			return "", []string{}, errors.Wrap(err, "unmarshalling gcloud response when returning managed-zone nameservers")
   227  		}
   228  		nameServers = managedZone.NameServers
   229  	} else {
   230  		log.Logger().Infof("Managed Zone doesn't exist for %s domain.", domain)
   231  	}
   232  	return managedZoneName, nameServers, nil
   233  }
   234  
   235  func getManagedZoneRecordSet(parentProject string, parentZone string, mzRecordSet recordSet) (recordSet, error) {
   236  	var googleRecordSet recordSet
   237  
   238  	args := []string{"dns",
   239  		"record-sets",
   240  		fmt.Sprintf("--project=%s", parentProject),
   241  		"list",
   242  		fmt.Sprintf("--name=%s", mzRecordSet.Name),
   243  		fmt.Sprintf("--zone=%s", parentZone),
   244  		fmt.Sprintf("--filter=type:%s", mzRecordSet.Type),
   245  		"--format=json",
   246  	}
   247  	cmd := util.Command{
   248  		Name: "gcloud",
   249  		Args: args,
   250  	}
   251  
   252  	output, err := cmd.Run()
   253  	if err != nil {
   254  		return googleRecordSet, errors.Wrap(err, "executing gcloud dns managed-zones list command")
   255  	}
   256  	var recordSets []recordSet
   257  	err = yaml.Unmarshal([]byte(output), &recordSets)
   258  	if err != nil {
   259  		return googleRecordSet, errors.Wrap(err, "unmarshalling gcloud response")
   260  	}
   261  
   262  	if len(recordSets) == 1 {
   263  		log.Logger().Infof("google dns record-set contains - domain: %s, with nameServers: %s\n", recordSets[0].Name, strings.Join(recordSets[0].Rrdatas, " "))
   264  		googleRecordSet = recordSets[0]
   265  	} else {
   266  		log.Logger().Debugf("No record-set or more than one record-set found for %s in %s", mzRecordSet.Name, parentZone)
   267  	}
   268  
   269  	return googleRecordSet, nil
   270  }
   271  
   272  func applyManagedZoneRecordTTL(parentProject string, parentZone string, googleRecordSet recordSet, ttl int) error {
   273  
   274  	if googleRecordSet.Name != "" {
   275  		// transaction start
   276  		startArgs := []string{"dns",
   277  			"record-sets",
   278  			fmt.Sprintf("--project=%s", parentProject),
   279  			"transaction",
   280  			"start",
   281  			fmt.Sprintf("--zone=%s", parentZone),
   282  			"--format=json",
   283  		}
   284  		startCmd := util.Command{
   285  			Name: "gcloud",
   286  			Args: startArgs,
   287  		}
   288  
   289  		_, err := startCmd.RunWithoutRetry()
   290  		if err != nil {
   291  			return errors.Wrap(err, "executing gcloud dns record-sets transaction start command")
   292  		}
   293  
   294  		// remove the previous record as it needs to be updated
   295  		removeArgs1 := []string{"dns",
   296  			"record-sets",
   297  			fmt.Sprintf("--project=%s", parentProject),
   298  			"transaction",
   299  			"remove",
   300  		}
   301  		removeArgs2 := []string{fmt.Sprintf("--name=%s", googleRecordSet.Name),
   302  			fmt.Sprintf("--ttl=%d", googleRecordSet.TTL),
   303  			fmt.Sprintf("--type=%s", googleRecordSet.Type),
   304  			fmt.Sprintf("--zone=%s", parentZone),
   305  			"--format=json",
   306  		}
   307  		removeArgs := append(removeArgs1, googleRecordSet.Rrdatas...)
   308  		removeArgs = append(removeArgs, removeArgs2...)
   309  
   310  		removeCmd := util.Command{
   311  			Name: "gcloud",
   312  			Args: removeArgs,
   313  		}
   314  
   315  		_, err = removeCmd.RunWithoutRetry()
   316  		if err != nil {
   317  			return errors.Wrap(err, "executing gcloud dns record-sets transaction remove command")
   318  		}
   319  
   320  		// transaction add
   321  		addArgs1 := []string{"dns",
   322  			"record-sets",
   323  			fmt.Sprintf("--project=%s", parentProject),
   324  			"transaction",
   325  			"add",
   326  		}
   327  		addArgs2 := []string{fmt.Sprintf("--name=%s", googleRecordSet.Name),
   328  			fmt.Sprintf("--ttl=%d", ttl),
   329  			fmt.Sprintf("--type=%s", googleRecordSet.Type),
   330  			fmt.Sprintf("--zone=%s", parentZone),
   331  			"--format=json",
   332  		}
   333  		addArgs := append(addArgs1, googleRecordSet.Rrdatas...)
   334  		addArgs = append(addArgs, addArgs2...)
   335  
   336  		addCmd := util.Command{
   337  			Name: "gcloud",
   338  			Args: addArgs,
   339  		}
   340  
   341  		_, err = addCmd.RunWithoutRetry()
   342  		if err != nil {
   343  			return errors.Wrap(err, "executing gcloud dns record-sets transaction add command")
   344  		}
   345  
   346  		// transaction execute
   347  		executeArgs := []string{"dns",
   348  			"record-sets",
   349  			fmt.Sprintf("--project=%s", parentProject),
   350  			"transaction",
   351  			"execute",
   352  			fmt.Sprintf("--zone=%s", parentZone),
   353  			"--format=json",
   354  		}
   355  		executeCmd := util.Command{
   356  			Name: "gcloud",
   357  			Args: executeArgs,
   358  		}
   359  
   360  		_, err = executeCmd.RunWithoutRetry()
   361  		if err != nil {
   362  			return errors.Wrap(err, "executing gcloud dns record-sets transaction start command")
   363  		}
   364  	}
   365  	return nil
   366  }
   367  
   368  func updateManagedZoneRecordTTL(projectID string, domain string, managedZone string, dnsType string, ttl int) error {
   369  	var managedZoneRecord recordSet
   370  	managedZoneRecord.Name = addDomainSuffix(domain)
   371  	managedZoneRecord.Type = dnsType
   372  	managedZoneRecord.TTL = ttl
   373  	managedZoneRecordSet, err := getManagedZoneRecordSet(projectID, managedZone, managedZoneRecord)
   374  	if err != nil {
   375  		return errors.Wrap(err, fmt.Sprintf("when retrieving the '%s' record-set of type '%s'", domain, dnsType))
   376  	}
   377  
   378  	err = applyManagedZoneRecordTTL(projectID, managedZone, managedZoneRecordSet, 60)
   379  	if err != nil {
   380  		return errors.Wrap(err, fmt.Sprintf("when updating the '%s' record-set of type '%s'", domain, dnsType))
   381  	}
   382  	return nil
   383  }
   384  
   385  // ClusterZone retrives the zone of GKE cluster description
   386  func (g *GCloud) ClusterZone(cluster string) (string, error) {
   387  	args := []string{"container",
   388  		"clusters",
   389  		"describe",
   390  		cluster}
   391  
   392  	cmd := util.Command{
   393  		Name: "gcloud",
   394  		Args: args,
   395  	}
   396  	output, err := cmd.RunWithoutRetry()
   397  	if err != nil {
   398  		return "", err
   399  	}
   400  
   401  	zone, err := parseClusterZone(output)
   402  	if err != nil {
   403  		return "", err
   404  	}
   405  	return zone, nil
   406  }
   407  
   408  func parseClusterZone(clusterInfo string) (string, error) {
   409  	ci := struct {
   410  		Zone string `json:"zone"`
   411  	}{}
   412  
   413  	err := yaml.Unmarshal([]byte(clusterInfo), &ci)
   414  	if err != nil {
   415  		return "", errors.Wrap(err, "extracting cluster zone from cluster info")
   416  	}
   417  	return ci.Zone, nil
   418  }
   419  
   420  type nodeConfig struct {
   421  	OauthScopes []string `json:"oauthScopes"`
   422  }
   423  
   424  func parseScopes(clusterInfo string) ([]string, error) {
   425  
   426  	ci := struct {
   427  		NodeConfig nodeConfig `json:"nodeConfig"`
   428  	}{}
   429  
   430  	err := yaml.Unmarshal([]byte(clusterInfo), &ci)
   431  	if err != nil {
   432  		return nil, errors.Wrap(err, "extracting cluster oauthScopes from cluster info")
   433  	}
   434  	return ci.NodeConfig.OauthScopes, nil
   435  }
   436  
   437  // BucketExists checks if a Google Storage bucket exists
   438  func (g *GCloud) BucketExists(projectID string, bucketName string) (bool, error) {
   439  	fullBucketName := fmt.Sprintf("gs://%s", bucketName)
   440  	args := []string{"ls"}
   441  
   442  	if projectID != "" {
   443  		args = append(args, "-p")
   444  		args = append(args, projectID)
   445  	}
   446  
   447  	cmd := util.Command{
   448  		Name: "gsutil",
   449  		Args: args,
   450  	}
   451  	output, err := cmd.Run()
   452  	if err != nil {
   453  		log.Logger().Infof("Error checking bucket exists: %s, %s", output, err)
   454  		return false, err
   455  	}
   456  	return strings.Contains(output, fullBucketName), nil
   457  }
   458  
   459  // ListObjects checks if a Google Storage bucket exists
   460  func (g *GCloud) ListObjects(bucketName string, path string) ([]string, error) {
   461  	fullBucketName := fmt.Sprintf("gs://%s/%s", bucketName, path)
   462  	args := []string{"ls", fullBucketName}
   463  
   464  	cmd := util.Command{
   465  		Name: "gsutil",
   466  		Args: args,
   467  	}
   468  	output, err := cmd.RunWithoutRetry()
   469  	if err != nil {
   470  		log.Logger().Infof("Error checking bucket exists: %s, %s", output, err)
   471  		return []string{}, err
   472  	}
   473  	return strings.Split(output, "\n"), nil
   474  }
   475  
   476  // CreateBucket creates a new Google Storage bucket
   477  func (g *GCloud) CreateBucket(projectID string, bucketName string, location string) error {
   478  	fullBucketName := fmt.Sprintf("gs://%s", bucketName)
   479  	args := []string{"mb", "-l", location}
   480  
   481  	if projectID != "" {
   482  		args = append(args, "-p")
   483  		args = append(args, projectID)
   484  	}
   485  
   486  	args = append(args, fullBucketName)
   487  
   488  	cmd := util.Command{
   489  		Name: "gsutil",
   490  		Args: args,
   491  	}
   492  	output, err := cmd.RunWithoutRetry()
   493  	if err != nil {
   494  		log.Logger().Infof("Error creating bucket: %s, %s", output, err)
   495  		return err
   496  	}
   497  	return nil
   498  }
   499  
   500  //AddBucketLabel adds a label to a Google Storage bucket
   501  func (g *GCloud) AddBucketLabel(bucketName string, label string) {
   502  	found := g.FindBucket(bucketName)
   503  	if found && label != "" {
   504  		fullBucketName := fmt.Sprintf("gs://%s", bucketName)
   505  		args := []string{"label", "ch", "-l", label}
   506  
   507  		args = append(args, fullBucketName)
   508  
   509  		cmd := util.Command{
   510  			Name: "gsutil",
   511  			Args: args,
   512  		}
   513  		output, err := cmd.RunWithoutRetry()
   514  		if err != nil {
   515  			log.Logger().Infof("Error adding bucket label: %s, %s", output, err)
   516  		}
   517  	}
   518  }
   519  
   520  // FindBucket finds a Google Storage bucket
   521  func (g *GCloud) FindBucket(bucketName string) bool {
   522  	fullBucketName := fmt.Sprintf("gs://%s", bucketName)
   523  	args := []string{"list", "-b", fullBucketName}
   524  
   525  	cmd := util.Command{
   526  		Name: "gsutil",
   527  		Args: args,
   528  	}
   529  	_, err := cmd.RunWithoutRetry()
   530  	if err != nil {
   531  		return false
   532  	}
   533  	return true
   534  }
   535  
   536  // DeleteAllObjectsInBucket deletes all objects in a Google Storage bucket
   537  func (g *GCloud) DeleteAllObjectsInBucket(bucketName string) error {
   538  	found := g.FindBucket(bucketName)
   539  	if !found {
   540  		return nil // nothing to delete
   541  	}
   542  	fullBucketName := fmt.Sprintf("gs://%s", bucketName)
   543  	args := []string{"-m", "rm", "-r", fullBucketName}
   544  
   545  	cmd := util.Command{
   546  		Name: "gsutil",
   547  		Args: args,
   548  	}
   549  	_, err := cmd.RunWithoutRetry()
   550  	if err != nil {
   551  		return err
   552  	}
   553  	return nil
   554  }
   555  
   556  // StreamTransferFileFromBucket will perform a stream transfer from the GCS bucket to stdout and return a scanner
   557  // with the piped result
   558  func StreamTransferFileFromBucket(fullBucketURL string) (io.ReadCloser, error) {
   559  	bucketAccessible, err := isBucketAccessible(fullBucketURL)
   560  	if !bucketAccessible || err != nil {
   561  		return nil, errors.Wrap(err, "can't access bucket")
   562  	}
   563  
   564  	args := []string{"cp", fullBucketURL, "-"}
   565  	cmd := exec.Command("gsutil", args...)
   566  	stdout, err := cmd.StdoutPipe()
   567  	if err != nil {
   568  		return nil, errors.Wrap(err, "can't get cmd stdout")
   569  	}
   570  	err = cmd.Start()
   571  	if err != nil {
   572  		return nil, errors.Wrap(err, "error streaming the logs from bucket")
   573  	}
   574  	return stdout, nil
   575  }
   576  
   577  func isBucketAccessible(bucketURL string) (bool, error) {
   578  	args := []string{"stat", bucketURL}
   579  	cmd := exec.Command("gsutil", args...)
   580  
   581  	out, err := cmd.CombinedOutput()
   582  	if err != nil {
   583  		return false, errors.New(string(out))
   584  	}
   585  
   586  	return true, nil
   587  }
   588  
   589  // DeleteBucket deletes a Google storage bucket
   590  func (g *GCloud) DeleteBucket(bucketName string) error {
   591  	found := g.FindBucket(bucketName)
   592  	if !found {
   593  		return nil // nothing to delete
   594  	}
   595  	fullBucketName := fmt.Sprintf("gs://%s", bucketName)
   596  	args := []string{"rb", fullBucketName}
   597  
   598  	cmd := util.Command{
   599  		Name: "gsutil",
   600  		Args: args,
   601  	}
   602  	_, err := cmd.RunWithoutRetry()
   603  	if err != nil {
   604  		return err
   605  	}
   606  	return nil
   607  }
   608  
   609  // GetRegionFromZone parses the region from a GCP zone name. TODO: Return an error if the format of the zone is not correct
   610  func GetRegionFromZone(zone string) string {
   611  	firstDash := strings.Index(zone, "-")
   612  	lastDash := strings.LastIndex(zone, "-")
   613  	if firstDash == lastDash { // It's a region, not a zone
   614  		return zone
   615  	}
   616  	return zone[0:lastDash]
   617  }
   618  
   619  // FindServiceAccount checks if a service account exists
   620  func (g *GCloud) FindServiceAccount(serviceAccount string, projectID string) bool {
   621  	args := []string{"iam",
   622  		"service-accounts",
   623  		"list",
   624  		"--filter",
   625  		serviceAccount,
   626  		"--project",
   627  		projectID}
   628  
   629  	cmd := util.Command{
   630  		Name: "gcloud",
   631  		Args: args,
   632  	}
   633  	output, err := cmd.Run()
   634  	if err != nil {
   635  		return false
   636  	}
   637  
   638  	if output == "Listed 0 items." {
   639  		return false
   640  	}
   641  	return true
   642  }
   643  
   644  // GetOrCreateServiceAccount retrieves or creates a GCP service account. It will return the path to the file where the service
   645  // account token is stored
   646  func (g *GCloud) GetOrCreateServiceAccount(serviceAccount string, projectID string, clusterConfigDir string, roles []string) (string, error) {
   647  	if projectID == "" {
   648  		return "", errors.New("cannot get/create a service account without a projectId")
   649  	}
   650  
   651  	found := g.FindServiceAccount(serviceAccount, projectID)
   652  	if !found {
   653  		log.Logger().Infof("Unable to find service account %s, checking if we have enough permission to create", util.ColorInfo(serviceAccount))
   654  
   655  		// if it doesn't check to see if we have permissions to create (assign roles) to a service account
   656  		hasPerm, err := g.CheckPermission("resourcemanager.projects.setIamPolicy", projectID)
   657  		if err != nil {
   658  			return "", err
   659  		}
   660  
   661  		if !hasPerm {
   662  			return "", errors.New("User does not have the required role 'resourcemanager.projects.setIamPolicy' to configure a service account")
   663  		}
   664  
   665  		// create service
   666  		log.Logger().Infof("Creating service account %s", util.ColorInfo(serviceAccount))
   667  		args := []string{"iam",
   668  			"service-accounts",
   669  			"create",
   670  			serviceAccount,
   671  			"--project",
   672  			projectID,
   673  			"--display-name",
   674  			serviceAccount}
   675  
   676  		cmd := util.Command{
   677  			Name: "gcloud",
   678  			Args: args,
   679  		}
   680  		_, err = cmd.RunWithoutRetry()
   681  		if err != nil {
   682  			return "", err
   683  		}
   684  
   685  		// assign roles to service account
   686  		for _, role := range roles {
   687  			log.Logger().Infof("Assigning role %s", role)
   688  			args = []string{"projects",
   689  				"add-iam-policy-binding",
   690  				projectID,
   691  				"--member",
   692  				fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccount, projectID),
   693  				"--role",
   694  				role,
   695  				"--project",
   696  				projectID}
   697  
   698  			cmd := util.Command{
   699  				Name: "gcloud",
   700  				Args: args,
   701  			}
   702  			_, err := cmd.Run()
   703  			if err != nil {
   704  				return "", err
   705  			}
   706  		}
   707  
   708  	} else {
   709  		log.Logger().Info("Service Account exists")
   710  	}
   711  
   712  	err := os.MkdirAll(clusterConfigDir, os.ModePerm)
   713  	if err != nil {
   714  		return "", errors.Wrapf(err, "Failed to create directory: %s", clusterConfigDir)
   715  	}
   716  	keyPath := filepath.Join(clusterConfigDir, fmt.Sprintf("%s.key.json", serviceAccount))
   717  
   718  	if _, err := os.Stat(keyPath); os.IsNotExist(err) {
   719  		log.Logger().Info("Downloading service account key")
   720  		err := g.CreateServiceAccountKey(serviceAccount, projectID, keyPath)
   721  		if err != nil {
   722  			log.Logger().Infof("Exceeds the maximum number of keys on service account %s",
   723  				util.ColorInfo(serviceAccount))
   724  			err := g.CleanupServiceAccountKeys(serviceAccount, projectID)
   725  			if err != nil {
   726  				return "", errors.Wrap(err, "cleaning up the service account keys")
   727  			}
   728  			err = g.CreateServiceAccountKey(serviceAccount, projectID, keyPath)
   729  			if err != nil {
   730  				return "", errors.Wrap(err, "creating service account key")
   731  			}
   732  		}
   733  	} else {
   734  		log.Logger().Info("Key already exists")
   735  	}
   736  
   737  	return keyPath, nil
   738  }
   739  
   740  // ConfigureBucketRoles gives the given roles to the given service account
   741  func (g *GCloud) ConfigureBucketRoles(projectID string, serviceAccount string, bucketURL string, roles []string) error {
   742  	member := fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccount, projectID)
   743  
   744  	bindings := bucketMemberRoles{}
   745  	for _, role := range roles {
   746  		bindings.Bindings = append(bindings.Bindings, memberRole{
   747  			Members: []string{member},
   748  			Role:    role,
   749  		})
   750  	}
   751  	file, err := ioutil.TempFile("", "gcp-iam-roles-")
   752  	if err != nil {
   753  		return errors.Wrapf(err, "failed to create temp file")
   754  	}
   755  	fileName := file.Name()
   756  
   757  	data, err := json.Marshal(&bindings)
   758  	if err != nil {
   759  		return errors.Wrapf(err, "failed to convert bindings %#v to JSON", bindings)
   760  	}
   761  	log.Logger().Infof("created json %s", string(data))
   762  	err = ioutil.WriteFile(fileName, data, util.DefaultWritePermissions)
   763  	if err != nil {
   764  		return errors.Wrapf(err, "failed to save bindings %#v to JSON file %s", bindings, fileName)
   765  	}
   766  	log.Logger().Infof("generated IAM bindings file %s", fileName)
   767  	args := []string{
   768  		"-m",
   769  		"iam",
   770  		"set",
   771  		"-a",
   772  		fileName,
   773  		bucketURL,
   774  	}
   775  	cmd := util.Command{
   776  		Name: "gsutil",
   777  		Args: args,
   778  	}
   779  	log.Logger().Infof("running: gsutil %s", strings.Join(args, " "))
   780  	_, err = cmd.Run()
   781  	if err != nil {
   782  		return err
   783  	}
   784  	return nil
   785  }
   786  
   787  type bucketMemberRoles struct {
   788  	Bindings []memberRole `json:"bindings"`
   789  }
   790  
   791  type memberRole struct {
   792  	Members []string `json:"members"`
   793  	Role    string   `json:"role"`
   794  }
   795  
   796  // CreateServiceAccountKey creates a new service account key and downloads into the given file
   797  func (g *GCloud) CreateServiceAccountKey(serviceAccount string, projectID string, keyPath string) error {
   798  	args := []string{"iam",
   799  		"service-accounts",
   800  		"keys",
   801  		"create",
   802  		keyPath,
   803  		"--iam-account",
   804  		fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID),
   805  		"--project",
   806  		projectID}
   807  
   808  	cmd := util.Command{
   809  		Name: "gcloud",
   810  		Args: args,
   811  	}
   812  	_, err := cmd.RunWithoutRetry()
   813  	if err != nil {
   814  		return errors.Wrap(err, "creating a new service account key")
   815  	}
   816  	return nil
   817  }
   818  
   819  // GetServiceAccountKeys returns all keys of a service account
   820  func (g *GCloud) GetServiceAccountKeys(serviceAccount string, projectID string) ([]string, error) {
   821  	keys := []string{}
   822  	account := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID)
   823  	args := []string{"iam",
   824  		"service-accounts",
   825  		"keys",
   826  		"list",
   827  		"--iam-account",
   828  		account,
   829  		"--project",
   830  		projectID}
   831  	cmd := util.Command{
   832  		Name: "gcloud",
   833  		Args: args,
   834  	}
   835  	output, err := cmd.RunWithoutRetry()
   836  	if err != nil {
   837  		return keys, errors.Wrapf(err, "listing the keys of the service account '%s'", account)
   838  	}
   839  
   840  	scanner := bufio.NewScanner(strings.NewReader(output))
   841  	// Skip the first line with the header information
   842  	scanner.Scan()
   843  	for scanner.Scan() {
   844  		keyFields := strings.Fields(scanner.Text())
   845  		if len(keyFields) > 0 {
   846  			keys = append(keys, keyFields[0])
   847  		}
   848  	}
   849  	return keys, nil
   850  }
   851  
   852  // ListClusters returns the clusters in a GKE project
   853  func (g *GCloud) ListClusters(region string, projectID string) ([]Cluster, error) {
   854  	args := []string{"container", "clusters", "list", "--region=" + region, "--format=json", "--quiet"}
   855  	if projectID != "" {
   856  		args = append(args, "--project="+projectID)
   857  	}
   858  	cmd := util.Command{
   859  		Name: "gcloud",
   860  		Args: args,
   861  	}
   862  	output, err := cmd.RunWithoutRetry()
   863  	if err != nil {
   864  		return nil, err
   865  	}
   866  
   867  	clusters := make([]Cluster, 0)
   868  	err = json.Unmarshal([]byte(output), &clusters)
   869  	if err != nil {
   870  		return nil, err
   871  	}
   872  	return clusters, nil
   873  }
   874  
   875  // LoadGkeCluster load a gke cluster from a GKE project
   876  func (g *GCloud) LoadGkeCluster(region string, projectID string, clusterName string) (*Cluster, error) {
   877  	args := []string{"container", "clusters", "describe", clusterName, "--region=" + region, "--format=json", "--quiet"}
   878  	if projectID != "" {
   879  		args = append(args, "--project="+projectID)
   880  	}
   881  	cmd := util.Command{
   882  		Name: "gcloud",
   883  		Args: args,
   884  	}
   885  	output, err := cmd.RunWithoutRetry()
   886  	if err != nil {
   887  		return nil, err
   888  	}
   889  
   890  	cluster := &Cluster{}
   891  	err = json.Unmarshal([]byte(output), cluster)
   892  	if err != nil {
   893  		return nil, err
   894  	}
   895  	return cluster, nil
   896  }
   897  
   898  // UpdateGkeClusterLabels updates labesl for a gke cluster
   899  func (g *GCloud) UpdateGkeClusterLabels(region string, projectID string, clusterName string, labels []string) error {
   900  	args := []string{"container", "clusters", "update", clusterName, "--quiet", "--update-labels=" + strings.Join(labels, ",") + ""}
   901  	if region != "" {
   902  		args = append(args, "--region="+region)
   903  	}
   904  	if projectID != "" {
   905  		args = append(args, "--project="+projectID)
   906  	}
   907  	cmd := util.Command{
   908  		Name: "gcloud",
   909  		Args: args,
   910  	}
   911  	_, err := cmd.RunWithoutRetry()
   912  	return err
   913  }
   914  
   915  // DeleteServiceAccountKey deletes a service account key
   916  func (g *GCloud) DeleteServiceAccountKey(serviceAccount string, projectID string, key string) error {
   917  	account := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID)
   918  	args := []string{"iam",
   919  		"service-accounts",
   920  		"keys",
   921  		"delete",
   922  		key,
   923  		"--iam-account",
   924  		account,
   925  		"--project",
   926  		projectID,
   927  		"--quiet"}
   928  	cmd := util.Command{
   929  		Name: "gcloud",
   930  		Args: args,
   931  	}
   932  	_, err := cmd.RunWithoutRetry()
   933  	if err != nil {
   934  		return errors.Wrapf(err, "deleting the key '%s'from service account '%s'", key, account)
   935  	}
   936  	return nil
   937  }
   938  
   939  // CleanupServiceAccountKeys remove all keys from given service account
   940  func (g *GCloud) CleanupServiceAccountKeys(serviceAccount string, projectID string) error {
   941  	keys, err := g.GetServiceAccountKeys(serviceAccount, projectID)
   942  	if err != nil {
   943  		return errors.Wrap(err, "retrieving the service account keys")
   944  	}
   945  
   946  	log.Logger().Infof("Cleaning up the keys of the service account %s", util.ColorInfo(serviceAccount))
   947  
   948  	for _, key := range keys {
   949  		err := g.DeleteServiceAccountKey(serviceAccount, projectID, key)
   950  		if err != nil {
   951  			log.Logger().Infof("Cannot delete the key %s from service account %s: %v",
   952  				util.ColorWarning(key), util.ColorInfo(serviceAccount), err)
   953  		} else {
   954  			log.Logger().Infof("Key %s was removed form service account %s",
   955  				util.ColorInfo(key), util.ColorInfo(serviceAccount))
   956  		}
   957  	}
   958  	return nil
   959  }
   960  
   961  // DeleteServiceAccount deletes a service account and its role bindings
   962  func (g *GCloud) DeleteServiceAccount(serviceAccount string, projectID string, roles []string) error {
   963  	found := g.FindServiceAccount(serviceAccount, projectID)
   964  	if !found {
   965  		return nil // nothing to delete
   966  	}
   967  	// remove roles to service account
   968  	for _, role := range roles {
   969  		log.Logger().Infof("Removing role %s", role)
   970  		args := []string{"projects",
   971  			"remove-iam-policy-binding",
   972  			projectID,
   973  			"--member",
   974  			fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccount, projectID),
   975  			"--role",
   976  			role,
   977  			"--project",
   978  			projectID}
   979  
   980  		cmd := util.Command{
   981  			Name: "gcloud",
   982  			Args: args,
   983  		}
   984  		_, err := cmd.RunWithoutRetry()
   985  		if err != nil {
   986  			return err
   987  		}
   988  	}
   989  	args := []string{"iam",
   990  		"service-accounts",
   991  		"delete",
   992  		fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID),
   993  		"--project",
   994  		projectID}
   995  
   996  	cmd := util.Command{
   997  		Name: "gcloud",
   998  		Args: args,
   999  	}
  1000  	_, err := cmd.RunWithoutRetry()
  1001  	if err != nil {
  1002  		return err
  1003  	}
  1004  	return nil
  1005  }
  1006  
  1007  // GetEnabledApis returns which services have the API enabled
  1008  func (g *GCloud) GetEnabledApis(projectID string) ([]string, error) {
  1009  	args := []string{"services", "list", "--enabled"}
  1010  
  1011  	if projectID != "" {
  1012  		args = append(args, "--project")
  1013  		args = append(args, projectID)
  1014  	}
  1015  
  1016  	apis := []string{}
  1017  
  1018  	cmd := util.Command{
  1019  		Name: "gcloud",
  1020  		Args: args,
  1021  	}
  1022  
  1023  	out, err := cmd.Run()
  1024  	if err != nil {
  1025  		return nil, err
  1026  	}
  1027  
  1028  	lines := strings.Split(out, "\n")
  1029  	for _, l := range lines {
  1030  		if strings.Contains(l, "NAME") {
  1031  			continue
  1032  		}
  1033  		fields := strings.Fields(l)
  1034  		apis = append(apis, fields[0])
  1035  	}
  1036  
  1037  	return apis, nil
  1038  }
  1039  
  1040  // EnableAPIs enables APIs for the given services
  1041  func (g *GCloud) EnableAPIs(projectID string, apis ...string) error {
  1042  	enabledApis, err := g.GetEnabledApis(projectID)
  1043  	if err != nil {
  1044  		return err
  1045  	}
  1046  
  1047  	toEnableArray := []string{}
  1048  
  1049  	for _, toEnable := range apis {
  1050  		fullName := fmt.Sprintf("%s.googleapis.com", toEnable)
  1051  		if !util.Contains(enabledApis, fullName) {
  1052  			toEnableArray = append(toEnableArray, fullName)
  1053  		}
  1054  	}
  1055  
  1056  	if len(toEnableArray) == 0 {
  1057  		log.Logger().Debugf("No apis need to be enable as they are already enabled: %s", util.ColorInfo(strings.Join(apis, " ")))
  1058  		return nil
  1059  	}
  1060  
  1061  	args := []string{"services", "enable"}
  1062  	args = append(args, toEnableArray...)
  1063  
  1064  	if projectID != "" {
  1065  		args = append(args, "--project")
  1066  		args = append(args, projectID)
  1067  	}
  1068  
  1069  	log.Logger().Debugf("Lets ensure we have %s enabled on your project via: %s", toEnableArray, util.ColorInfo("gcloud "+strings.Join(args, " ")))
  1070  
  1071  	cmd := util.Command{
  1072  		Name: "gcloud",
  1073  		Args: args,
  1074  	}
  1075  	_, err = cmd.RunWithoutRetry()
  1076  	if err != nil {
  1077  		return err
  1078  	}
  1079  	return nil
  1080  }
  1081  
  1082  // Login login an user into Google account. It skips the interactive login using the
  1083  // browser when the skipLogin flag is active
  1084  func (g *GCloud) Login(serviceAccountKeyPath string, skipLogin bool) error {
  1085  	if serviceAccountKeyPath != "" {
  1086  		log.Logger().Infof("Activating service account %s", util.ColorInfo(serviceAccountKeyPath))
  1087  
  1088  		if _, err := os.Stat(serviceAccountKeyPath); os.IsNotExist(err) {
  1089  			return errors.New("Unable to locate service account " + serviceAccountKeyPath)
  1090  		}
  1091  
  1092  		cmd := util.Command{
  1093  			Name: "gcloud",
  1094  			Args: []string{"auth", "activate-service-account", "--key-file", serviceAccountKeyPath},
  1095  		}
  1096  		_, err := cmd.RunWithoutRetry()
  1097  		if err != nil {
  1098  			return err
  1099  		}
  1100  
  1101  		// GCP IAM changes can take up to 80 seconds to propagate
  1102  		err = retry(10, 10*time.Second, func() error {
  1103  			log.Logger().Infof("Checking for readiness...")
  1104  
  1105  			projects, err := GetGoogleProjects()
  1106  			if err != nil {
  1107  				return err
  1108  			}
  1109  
  1110  			if len(projects) == 0 {
  1111  				return errors.New("service account not ready yet")
  1112  			}
  1113  
  1114  			return nil
  1115  		})
  1116  		if err != nil {
  1117  			return err
  1118  		}
  1119  
  1120  	} else if !skipLogin {
  1121  		cmd := util.Command{
  1122  			Name: "gcloud",
  1123  			Args: []string{"auth", "login", "--brief"},
  1124  		}
  1125  		_, err := cmd.RunWithoutRetry()
  1126  		if err != nil {
  1127  			return err
  1128  		}
  1129  	}
  1130  	return nil
  1131  }
  1132  
  1133  func retry(attempts int, sleep time.Duration, fn func() error) error {
  1134  	if err := fn(); err != nil {
  1135  		if s, ok := err.(stop); ok {
  1136  			// Return the original error for later checking
  1137  			return s.error
  1138  		}
  1139  
  1140  		if attempts--; attempts > 0 {
  1141  			time.Sleep(sleep)
  1142  			return retry(attempts, 2*sleep, fn)
  1143  		}
  1144  		return err
  1145  	}
  1146  	return nil
  1147  }
  1148  
  1149  type stop struct {
  1150  	error
  1151  }
  1152  
  1153  // CheckPermission checks permission on the given project
  1154  func (g *GCloud) CheckPermission(perm string, projectID string) (bool, error) {
  1155  	if projectID == "" {
  1156  		return false, errors.New("cannot check permission without a projectId")
  1157  	}
  1158  	// if it doesn't check to see if we have permissions to create (assign roles) to a service account
  1159  	args := []string{"iam",
  1160  		"list-testable-permissions",
  1161  		fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectID),
  1162  		"--filter",
  1163  		perm}
  1164  
  1165  	cmd := util.Command{
  1166  		Name: "gcloud",
  1167  		Args: args,
  1168  	}
  1169  	output, err := cmd.RunWithoutRetry()
  1170  	if err != nil {
  1171  		return false, err
  1172  	}
  1173  
  1174  	return strings.Contains(output, perm), nil
  1175  }
  1176  
  1177  // CreateKmsKeyring creates a new KMS keyring
  1178  func (g *GCloud) CreateKmsKeyring(keyringName string, projectID string) error {
  1179  	if keyringName == "" {
  1180  		return errors.New("provided keyring name is empty")
  1181  	}
  1182  
  1183  	if g.IsKmsKeyringAvailable(keyringName, projectID) {
  1184  		log.Logger().Debugf("keyring '%s' already exists", keyringName)
  1185  		return nil
  1186  	}
  1187  
  1188  	args := []string{"kms",
  1189  		"keyrings",
  1190  		"create",
  1191  		keyringName,
  1192  		"--location",
  1193  		KmsLocation,
  1194  		"--project",
  1195  		projectID,
  1196  	}
  1197  
  1198  	log.Logger().Debugf("creating keyring '%s' project=%s, location=%s", keyringName, projectID, KmsLocation)
  1199  
  1200  	cmd := util.Command{
  1201  		Name: "gcloud",
  1202  		Args: args,
  1203  	}
  1204  	_, err := cmd.RunWithoutRetry()
  1205  	if err != nil {
  1206  		return errors.Wrap(err, "creating kms keyring")
  1207  	}
  1208  	return nil
  1209  }
  1210  
  1211  // IsKmsKeyringAvailable checks if the KMS keyring is already available
  1212  func (g *GCloud) IsKmsKeyringAvailable(keyringName string, projectID string) bool {
  1213  	log.Logger().Debugf("IsKmsKeyringAvailable keyring=%s, projectId=%s, location=%s", keyringName, projectID, KmsLocation)
  1214  	args := []string{"kms",
  1215  		"keyrings",
  1216  		"describe",
  1217  		keyringName,
  1218  		"--location",
  1219  		KmsLocation,
  1220  		"--project",
  1221  		projectID,
  1222  	}
  1223  
  1224  	cmd := util.Command{
  1225  		Name: "gcloud",
  1226  		Args: args,
  1227  	}
  1228  	_, err := cmd.RunWithoutRetry()
  1229  	if err != nil {
  1230  		return false
  1231  	}
  1232  	return true
  1233  }
  1234  
  1235  // CreateKmsKey creates a new KMS key in the given keyring
  1236  func (g *GCloud) CreateKmsKey(keyName string, keyringName string, projectID string) error {
  1237  	if g.IsKmsKeyAvailable(keyName, keyringName, projectID) {
  1238  		log.Logger().Debugf("key '%s' already exists", keyName)
  1239  		return nil
  1240  	}
  1241  
  1242  	log.Logger().Debugf("creating key '%s' keyring=%s, project=%s, location=%s", keyName, keyringName, projectID, KmsLocation)
  1243  
  1244  	args := []string{"kms",
  1245  		"keys",
  1246  		"create",
  1247  		keyName,
  1248  		"--location",
  1249  		KmsLocation,
  1250  		"--keyring",
  1251  		keyringName,
  1252  		"--purpose",
  1253  		"encryption",
  1254  		"--project",
  1255  		projectID,
  1256  	}
  1257  	cmd := util.Command{
  1258  		Name: "gcloud",
  1259  		Args: args,
  1260  	}
  1261  	_, err := cmd.RunWithoutRetry()
  1262  	if err != nil {
  1263  		return errors.Wrapf(err, "creating kms key '%s' into keyring '%s'", keyName, keyringName)
  1264  	}
  1265  	return nil
  1266  }
  1267  
  1268  // IsKmsKeyAvailable checks if the KMS key is already available
  1269  func (g *GCloud) IsKmsKeyAvailable(keyName string, keyringName string, projectID string) bool {
  1270  	log.Logger().Debugf("IsKmsKeyAvailable keyName=%s, keyring=%s, projectId=%s, location=%s", keyName, keyringName, projectID, KmsLocation)
  1271  
  1272  	args := []string{"kms",
  1273  		"keys",
  1274  		"describe",
  1275  		keyName,
  1276  		"--location",
  1277  		KmsLocation,
  1278  		"--keyring",
  1279  		keyringName,
  1280  		"--project",
  1281  		projectID,
  1282  	}
  1283  
  1284  	cmd := util.Command{
  1285  		Name: "gcloud",
  1286  		Args: args,
  1287  	}
  1288  	_, err := cmd.RunWithoutRetry()
  1289  	if err != nil {
  1290  		return false
  1291  	}
  1292  	return true
  1293  }
  1294  
  1295  // IsGCSWriteRoleEnabled will check if the devstorage.full_control scope is enabled in the cluster in order to use GCS
  1296  func (g *GCloud) IsGCSWriteRoleEnabled(cluster string, zone string) (bool, error) {
  1297  	args := []string{"container",
  1298  		"clusters",
  1299  		"describe",
  1300  		cluster,
  1301  		"--zone",
  1302  		zone}
  1303  
  1304  	cmd := util.Command{
  1305  		Name: "gcloud",
  1306  		Args: args,
  1307  	}
  1308  	output, err := cmd.RunWithoutRetry()
  1309  	if err != nil {
  1310  		return false, err
  1311  	}
  1312  
  1313  	oauthScopes, err := parseScopes(output)
  1314  	if err != nil {
  1315  		return false, err
  1316  	}
  1317  
  1318  	for _, s := range oauthScopes {
  1319  		if strings.Contains(s, "devstorage.full_control") {
  1320  			return true, nil
  1321  		}
  1322  	}
  1323  	return false, nil
  1324  }
  1325  
  1326  // ConnectToCluster connects to the specified cluster
  1327  func (g *GCloud) ConnectToCluster(projectID, zone, clusterName string) error {
  1328  	args := []string{"container",
  1329  		"clusters",
  1330  		"get-credentials",
  1331  		clusterName,
  1332  		"--zone",
  1333  		zone,
  1334  		"--project", projectID}
  1335  
  1336  	cmd := util.Command{
  1337  		Name: "gcloud",
  1338  		Args: args,
  1339  	}
  1340  	_, err := cmd.RunWithoutRetry()
  1341  	if err != nil {
  1342  		return errors.Wrapf(err, "failed to connect to cluster %s", clusterName)
  1343  	}
  1344  	return nil
  1345  }
  1346  
  1347  // ConnectToRegionCluster connects to the specified regional cluster
  1348  func (g *GCloud) ConnectToRegionCluster(projectID, region, clusterName string) error {
  1349  	args := []string{"container",
  1350  		"clusters",
  1351  		"get-credentials",
  1352  		clusterName,
  1353  		"--region",
  1354  		region,
  1355  		"--project", projectID}
  1356  
  1357  	cmd := util.Command{
  1358  		Name: "gcloud",
  1359  		Args: args,
  1360  	}
  1361  	_, err := cmd.RunWithoutRetry()
  1362  	if err != nil {
  1363  		return errors.Wrapf(err, "failed to connect to region cluster %s", clusterName)
  1364  	}
  1365  	return nil
  1366  }
  1367  
  1368  // UserLabel returns a string identifying current user that can be used as a label
  1369  func (g *GCloud) UserLabel() string {
  1370  	user, err := osUser.Current()
  1371  	if err == nil && user != nil && user.Username != "" {
  1372  		userLabel := util.SanitizeLabel(user.Username)
  1373  		return fmt.Sprintf("created-by:%s", userLabel)
  1374  	}
  1375  	return ""
  1376  }
  1377  
  1378  // CreateGCPServiceAccount creates a service account in GCP for a service using the account roles specified
  1379  func (g *GCloud) CreateGCPServiceAccount(kubeClient kubernetes.Interface, serviceName, serviceAbbreviation, namespace, clusterName, projectID string, serviceAccountRoles []string, serviceAccountSecretKey string) (string, error) {
  1380  	serviceAccountDir, err := ioutil.TempDir("", "gke")
  1381  	if err != nil {
  1382  		return "", errors.Wrap(err, "creating a temporary folder where the service account will be stored")
  1383  	}
  1384  	defer os.RemoveAll(serviceAccountDir)
  1385  
  1386  	serviceAccountName := naming.ToValidGCPServiceAccount(ServiceAccountName(clusterName, serviceAbbreviation))
  1387  
  1388  	serviceAccountPath, err := g.GetOrCreateServiceAccount(serviceAccountName, projectID, serviceAccountDir, serviceAccountRoles)
  1389  	if err != nil {
  1390  		return "", errors.Wrap(err, "creating the service account")
  1391  	}
  1392  
  1393  	secretName, err := g.storeGCPServiceAccountIntoSecret(kubeClient, serviceAccountPath, serviceName, namespace, serviceAccountSecretKey)
  1394  	if err != nil {
  1395  		return "", errors.Wrap(err, "storing the service account into a secret")
  1396  	}
  1397  	return secretName, nil
  1398  }
  1399  
  1400  func (g *GCloud) storeGCPServiceAccountIntoSecret(client kubernetes.Interface, serviceAccountPath, serviceName, namespace string, serviceAccountSecretKey string) (string, error) {
  1401  	serviceAccount, err := ioutil.ReadFile(serviceAccountPath)
  1402  	if err != nil {
  1403  		return "", errors.Wrapf(err, "reading the service account from file '%s'", serviceAccountPath)
  1404  	}
  1405  
  1406  	secretName := GcpServiceAccountSecretName(serviceName)
  1407  	secret := &v1.Secret{
  1408  		ObjectMeta: metav1.ObjectMeta{
  1409  			Name: secretName,
  1410  		},
  1411  		Data: map[string][]byte{
  1412  			serviceAccountSecretKey: serviceAccount,
  1413  		},
  1414  	}
  1415  
  1416  	secrets := client.CoreV1().Secrets(namespace)
  1417  	_, err = secrets.Get(secretName, metav1.GetOptions{})
  1418  	if err != nil {
  1419  		_, err = secrets.Create(secret)
  1420  	} else {
  1421  		_, err = secrets.Update(secret)
  1422  	}
  1423  	return secretName, nil
  1424  }
  1425  
  1426  // CurrentProject returns the current GKE project name if it can be detected
  1427  func (g *GCloud) CurrentProject() (string, error) {
  1428  	args := []string{"config",
  1429  		"list",
  1430  		"--format",
  1431  		"value(core.project)",
  1432  	}
  1433  
  1434  	cmd := util.Command{
  1435  		Name: "gcloud",
  1436  		Args: args,
  1437  	}
  1438  	text, err := cmd.RunWithoutRetry()
  1439  	if err != nil {
  1440  		return text, errors.Wrap(err, "failed to detect the current GCP project")
  1441  	}
  1442  	return strings.TrimSpace(text), nil
  1443  }
  1444  
  1445  func (g *GCloud) GetProjectNumber(projectID string) (string, error) {
  1446  	args := []string{
  1447  		"projects",
  1448  		"describe",
  1449  		projectID,
  1450  		"--format=json",
  1451  	}
  1452  	cmd := util.Command{
  1453  		Name: "gcloud",
  1454  		Args: args,
  1455  	}
  1456  
  1457  	log.Logger().Infof("running: gcloud %s", strings.Join(args, " "))
  1458  	output, err := cmd.Run()
  1459  	if err != nil {
  1460  		return "", err
  1461  	}
  1462  
  1463  	var project project
  1464  	err = json.Unmarshal([]byte(output), &project)
  1465  	if err != nil {
  1466  		return "", errors.Wrapf(err, "failed to unmarshal %s", output)
  1467  	}
  1468  	return project.ProjectNumber, nil
  1469  }
  1470  
  1471  type project struct {
  1472  	ProjectNumber string `json:"projectNumber"`
  1473  }
  1474  
  1475  func addDomainSuffix(domain string) string {
  1476  	if domain[len(domain)-1:] != "." {
  1477  		return fmt.Sprintf("%s.", domain)
  1478  	}
  1479  	return domain
  1480  }