github.com/abayer/test-infra@v0.0.5/kubetest/gke.go (about)

     1  /*
     2  Copyright 2017 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 / gke.go provides the Google Container Engine (GKE)
    18  // kubetest deployer via newGKE().
    19  //
    20  // TODO(zmerlynn): Pull this out to a separate package?
    21  package main
    22  
    23  import (
    24  	"encoding/json"
    25  	"flag"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"log"
    29  	"os"
    30  	"os/exec"
    31  	"regexp"
    32  	"sort"
    33  	"strconv"
    34  	"strings"
    35  	"time"
    36  
    37  	"k8s.io/test-infra/kubetest/util"
    38  )
    39  
    40  const (
    41  	defaultPool   = "default"
    42  	e2eAllow      = "tcp:22,tcp:80,tcp:8080,tcp:30000-32767,udp:30000-32767"
    43  	defaultCreate = "container clusters create --quiet"
    44  )
    45  
    46  var (
    47  	gkeAdditionalZones             = flag.String("gke-additional-zones", "", "(gke only) List of additional Google Compute Engine zones to use. Clusters are created symmetrically across zones by default, see --gke-shape for details.")
    48  	gkeNodeLocations               = flag.String("gke-node-locations", "", "(gke only) List of Google Compute Engine zones to use.")
    49  	gkeEnvironment                 = flag.String("gke-environment", "", "(gke only) Container API endpoint to use, one of 'test', 'staging', 'prod', or a custom https:// URL")
    50  	gkeShape                       = flag.String("gke-shape", `{"default":{"Nodes":3,"MachineType":"n1-standard-2"}}`, `(gke only) A JSON description of node pools to create. The node pool 'default' is required and used for initial cluster creation. All node pools are symmetric across zones, so the cluster total node count is {total nodes in --gke-shape} * {1 + (length of --gke-additional-zones)}. Example: '{"default":{"Nodes":999,"MachineType:":"n1-standard-1"},"heapster":{"Nodes":1, "MachineType":"n1-standard-8"}}`)
    51  	gkeCreateArgs                  = flag.String("gke-create-args", "", "(gke only) (deprecated, use a modified --gke-create-command') Additional arguments passed directly to 'gcloud container clusters create'")
    52  	gkeCommandGroup                = flag.String("gke-command-group", "", "(gke only) Use a different gcloud track (e.g. 'alpha') for all 'gcloud container' commands. Note: This is added to --gke-create-command on create. You should only use --gke-command-group if you need to change the gcloud track for *every* gcloud container command.")
    53  	gkeCreateCommand               = flag.String("gke-create-command", defaultCreate, "(gke only) gcloud subcommand used to create a cluster. Modify if you need to pass arbitrary arguments to create.")
    54  	gkeCustomSubnet                = flag.String("gke-custom-subnet", "", "(gke only) if specified, we create a custom subnet with the specified options and use it for the gke cluster. The format should be '<subnet-name> --region=<subnet-gcp-region> --range=<subnet-cidr> <any other optional params>'.")
    55  	gkeSingleZoneNodeInstanceGroup = flag.Bool("gke-single-zone-node-instance-group", true, "(gke only) Add instance groups from a single zone to the NODE_INSTANCE_GROUP env variable.")
    56  
    57  	// poolRe matches instance group URLs of the form `https://www.googleapis.com/compute/v1/projects/some-project/zones/a-zone/instanceGroupManagers/gke-some-cluster-some-pool-90fcb815-grp`. Match meaning:
    58  	// m[0]: path starting with zones/
    59  	// m[1]: zone
    60  	// m[2]: pool name (passed to e2es)
    61  	// m[3]: unique hash (used as nonce for firewall rules)
    62  	poolRe = regexp.MustCompile(`zones/([^/]+)/instanceGroupManagers/(gke-.*-([0-9a-f]{8})-grp)$`)
    63  
    64  	urlRe = regexp.MustCompile(`https://.*/`)
    65  )
    66  
    67  type gkeNodePool struct {
    68  	Nodes       int
    69  	MachineType string
    70  }
    71  
    72  type gkeDeployer struct {
    73  	project                     string
    74  	zone                        string
    75  	region                      string
    76  	location                    string
    77  	additionalZones             string
    78  	nodeLocations               string
    79  	cluster                     string
    80  	shape                       map[string]gkeNodePool
    81  	network                     string
    82  	subnetwork                  string
    83  	subnetworkRegion            string
    84  	image                       string
    85  	imageFamily                 string
    86  	imageProject                string
    87  	commandGroup                []string
    88  	createCommand               []string
    89  	singleZoneNodeInstanceGroup bool
    90  
    91  	setup          bool
    92  	kubecfg        string
    93  	instanceGroups []*ig
    94  }
    95  
    96  type ig struct {
    97  	path string
    98  	zone string
    99  	name string
   100  	uniq string
   101  }
   102  
   103  var _ deployer = &gkeDeployer{}
   104  
   105  func newGKE(provider, project, zone, region, network, image, imageFamily, imageProject, cluster string, testArgs *string, upgradeArgs *string) (*gkeDeployer, error) {
   106  	if provider != "gke" {
   107  		return nil, fmt.Errorf("--provider must be 'gke' for GKE deployment, found %q", provider)
   108  	}
   109  	g := &gkeDeployer{}
   110  
   111  	if cluster == "" {
   112  		return nil, fmt.Errorf("--cluster must be set for GKE deployment")
   113  	}
   114  	g.cluster = cluster
   115  
   116  	if project == "" {
   117  		return nil, fmt.Errorf("--gcp-project must be set for GKE deployment")
   118  	}
   119  	g.project = project
   120  
   121  	if zone == "" && region == "" {
   122  		return nil, fmt.Errorf("--gcp-zone or --gcp-region must be set for GKE deployment")
   123  	} else if zone != "" && region != "" {
   124  		return nil, fmt.Errorf("--gcp-zone and --gcp-region cannot both be set")
   125  	}
   126  	if zone != "" {
   127  		g.zone = zone
   128  		g.location = "--zone=" + zone
   129  	} else if region != "" {
   130  		g.region = region
   131  		g.location = "--region=" + region
   132  	}
   133  
   134  	if network == "" {
   135  		return nil, fmt.Errorf("--gcp-network must be set for GKE deployment")
   136  	}
   137  	g.network = network
   138  
   139  	if image == "" {
   140  		return nil, fmt.Errorf("--gcp-node-image must be set for GKE deployment")
   141  	}
   142  	if strings.ToUpper(image) == "CUSTOM" {
   143  		if imageFamily == "" || imageProject == "" {
   144  			return nil, fmt.Errorf("--image-family and --image-project must be set for GKE deployment if --gcp-node-image=CUSTOM")
   145  		}
   146  	}
   147  	g.imageFamily = imageFamily
   148  	g.imageProject = imageProject
   149  	g.image = image
   150  
   151  	g.additionalZones = *gkeAdditionalZones
   152  	g.nodeLocations = *gkeNodeLocations
   153  
   154  	err := json.Unmarshal([]byte(*gkeShape), &g.shape)
   155  	if err != nil {
   156  		return nil, fmt.Errorf("--gke-shape must be valid JSON, unmarshal error: %v, JSON: %q", err, *gkeShape)
   157  	}
   158  	if _, ok := g.shape[defaultPool]; !ok {
   159  		return nil, fmt.Errorf("--gke-shape must include a node pool named 'default', found %q", *gkeShape)
   160  	}
   161  
   162  	g.commandGroup = strings.Fields(*gkeCommandGroup)
   163  
   164  	g.createCommand = append([]string{}, g.commandGroup...)
   165  	g.createCommand = append(g.createCommand, strings.Fields(*gkeCreateCommand)...)
   166  	createArgs := strings.Fields(*gkeCreateArgs)
   167  	if len(createArgs) > 0 {
   168  		log.Printf("--gke-create-args is deprecated, please use '--gke-create-command=%s %s'", defaultCreate, *gkeCreateArgs)
   169  	}
   170  	g.createCommand = append(g.createCommand, createArgs...)
   171  
   172  	if err := util.MigrateOptions([]util.MigratedOption{{
   173  		Env:    "CLOUDSDK_API_ENDPOINT_OVERRIDES_CONTAINER",
   174  		Option: gkeEnvironment,
   175  		Name:   "--gke-environment",
   176  	}}); err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	var endpoint string
   181  	switch env := *gkeEnvironment; {
   182  	case env == "test":
   183  		endpoint = "https://test-container.sandbox.googleapis.com/"
   184  	case env == "staging":
   185  		endpoint = "https://staging-container.sandbox.googleapis.com/"
   186  	case env == "prod":
   187  		endpoint = "https://container.googleapis.com/"
   188  	case urlRe.MatchString(env):
   189  		endpoint = env
   190  	default:
   191  		return nil, fmt.Errorf("--gke-environment must be one of {test,staging,prod} or match %v, found %q", urlRe, env)
   192  	}
   193  	if err := os.Setenv("CLOUDSDK_API_ENDPOINT_OVERRIDES_CONTAINER", endpoint); err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	// Override kubecfg to a temporary file rather than trashing the user's.
   198  	f, err := ioutil.TempFile("", "gke-kubecfg")
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	defer f.Close()
   203  	kubecfg := f.Name()
   204  	if err := f.Chmod(0600); err != nil {
   205  		return nil, err
   206  	}
   207  	g.kubecfg = kubecfg
   208  
   209  	// We want no KUBERNETES_PROVIDER, but to set
   210  	// KUBERNETES_CONFORMANCE_PROVIDER and
   211  	// KUBERNETES_CONFORMANCE_TEST. This prevents ginkgo-e2e.sh from
   212  	// using the cluster/gke functions.
   213  	//
   214  	// We do this in the deployer constructor so that
   215  	// cluster/gce/list-resources.sh outputs the same provider for the
   216  	// extent of the binary. (It seems like it belongs in TestSetup,
   217  	// but that way leads to madness.)
   218  	//
   219  	// TODO(zmerlynn): This is gross.
   220  	if err := os.Unsetenv("KUBERNETES_PROVIDER"); err != nil {
   221  		return nil, err
   222  	}
   223  	if err := os.Setenv("KUBERNETES_CONFORMANCE_TEST", "yes"); err != nil {
   224  		return nil, err
   225  	}
   226  	if err := os.Setenv("KUBERNETES_CONFORMANCE_PROVIDER", "gke"); err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	// TODO(zmerlynn): Another snafu of cluster/gke/list-resources.sh:
   231  	// Set KUBE_GCE_INSTANCE_PREFIX so that we don't accidentally pick
   232  	// up CLUSTER_NAME later.
   233  	if err := os.Setenv("KUBE_GCE_INSTANCE_PREFIX", "gke-"+g.cluster); err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	// set --num-nodes flag for ginkgo, since NUM_NODES is not set for gke deployer.
   238  	numNodes := strconv.Itoa(g.shape[defaultPool].Nodes)
   239  	// testArgs can be empty, and we need to support this case
   240  	*testArgs = strings.Join(util.SetFieldDefault(strings.Fields(*testArgs), "--num-nodes", numNodes), " ")
   241  
   242  	if *upgradeArgs != "" {
   243  		// --upgrade-target will be passed to e2e upgrade framework to get a valid update version.
   244  		// See usage from https://github.com/kubernetes/kubernetes/blob/master/hack/get-build.sh for supported targets.
   245  		// Here we special case for gke-latest and will extract an actual valid gke version.
   246  		// - gke-latest will be resolved to the latest gke version, and
   247  		// - gke-latest-1.7 will be resolved to the latest 1.7 patch version supported on gke.
   248  		fields, val, exist := util.ExtractField(strings.Fields(*upgradeArgs), "--upgrade-target")
   249  		if exist {
   250  			if strings.HasPrefix(val, "gke-latest") {
   251  				releasePrefix := ""
   252  				if strings.HasPrefix(val, "gke-latest-") {
   253  					releasePrefix = strings.TrimPrefix(val, "gke-latest-")
   254  				}
   255  				if val, err = getLatestGKEVersion(project, zone, region, releasePrefix); err != nil {
   256  					return nil, fmt.Errorf("fail to get latest gke version : %v", err)
   257  				}
   258  			}
   259  			fields = util.SetFieldDefault(fields, "--upgrade-target", val)
   260  		}
   261  		*upgradeArgs = strings.Join(util.SetFieldDefault(fields, "--num-nodes", numNodes), " ")
   262  	}
   263  
   264  	g.singleZoneNodeInstanceGroup = *gkeSingleZoneNodeInstanceGroup
   265  
   266  	return g, nil
   267  }
   268  
   269  func (g *gkeDeployer) Up() error {
   270  	// Create network if it doesn't exist.
   271  	if control.NoOutput(exec.Command("gcloud", "compute", "networks", "describe", g.network,
   272  		"--project="+g.project,
   273  		"--format=value(name)")) != nil {
   274  		// Assume error implies non-existent.
   275  		log.Printf("Couldn't describe network '%s', assuming it doesn't exist and creating it", g.network)
   276  		if err := control.FinishRunning(exec.Command("gcloud", "compute", "networks", "create", g.network,
   277  			"--project="+g.project,
   278  			"--subnet-mode=auto")); err != nil {
   279  			return err
   280  		}
   281  	}
   282  	// Create a custom subnet in that network if it was asked for.
   283  	if *gkeCustomSubnet != "" {
   284  		customSubnetFields := strings.Fields(*gkeCustomSubnet)
   285  		createSubnetCommand := []string{"compute", "networks", "subnets", "create"}
   286  		createSubnetCommand = append(createSubnetCommand, "--project="+g.project, "--network="+g.network)
   287  		createSubnetCommand = append(createSubnetCommand, customSubnetFields...)
   288  		if err := control.FinishRunning(exec.Command("gcloud", createSubnetCommand...)); err != nil {
   289  			return err
   290  		}
   291  		g.subnetwork = customSubnetFields[0]
   292  		g.subnetworkRegion = customSubnetFields[1]
   293  	}
   294  
   295  	def := g.shape[defaultPool]
   296  	args := make([]string, len(g.createCommand))
   297  	copy(args, g.createCommand)
   298  	args = append(args,
   299  		"--project="+g.project,
   300  		g.location,
   301  		"--machine-type="+def.MachineType,
   302  		"--image-type="+g.image,
   303  		"--num-nodes="+strconv.Itoa(def.Nodes),
   304  		"--network="+g.network,
   305  	)
   306  	if strings.ToUpper(g.image) == "CUSTOM" {
   307  		args = append(args, "--image-family="+g.imageFamily)
   308  		args = append(args, "--image-project="+g.imageProject)
   309  	}
   310  	if g.subnetwork != "" {
   311  		args = append(args, "--subnetwork="+g.subnetwork)
   312  	}
   313  	if g.additionalZones != "" {
   314  		args = append(args, "--additional-zones="+g.additionalZones)
   315  		if err := os.Setenv("MULTIZONE", "true"); err != nil {
   316  			return fmt.Errorf("error setting MULTIZONE env variable: %v", err)
   317  		}
   318  
   319  	}
   320  	if g.nodeLocations != "" {
   321  		args = append(args, "--node-locations="+g.nodeLocations)
   322  		numNodeLocations := strings.Split(g.nodeLocations, ",")
   323  		if len(numNodeLocations) > 1 {
   324  			if err := os.Setenv("MULTIZONE", "true"); err != nil {
   325  				return fmt.Errorf("error setting MULTIZONE env variable: %v", err)
   326  			}
   327  		}
   328  	}
   329  	// TODO(zmerlynn): The version should be plumbed through Extract
   330  	// or a separate flag rather than magic env variables.
   331  	if v := os.Getenv("CLUSTER_API_VERSION"); v != "" {
   332  		args = append(args, "--cluster-version="+v)
   333  	}
   334  	args = append(args, g.cluster)
   335  	if err := control.FinishRunning(exec.Command("gcloud", args...)); err != nil {
   336  		return fmt.Errorf("error creating cluster: %v", err)
   337  	}
   338  	for poolName, pool := range g.shape {
   339  		if poolName == defaultPool {
   340  			continue
   341  		}
   342  		if err := control.FinishRunning(exec.Command("gcloud", g.containerArgs(
   343  			"node-pools", "create", poolName,
   344  			"--cluster="+g.cluster,
   345  			"--project="+g.project,
   346  			g.location,
   347  			"--machine-type="+pool.MachineType,
   348  			"--num-nodes="+strconv.Itoa(pool.Nodes))...)); err != nil {
   349  			return fmt.Errorf("error creating node pool %q: %v", poolName, err)
   350  		}
   351  	}
   352  	return nil
   353  }
   354  
   355  func (g *gkeDeployer) IsUp() error {
   356  	return isUp(g)
   357  }
   358  
   359  // DumpClusterLogs for GKE generates a small script that wraps
   360  // log-dump.sh with the appropriate shell-fu to get the cluster
   361  // dumped.
   362  //
   363  // TODO(zmerlynn): This whole path is really gross, but this seemed
   364  // the least gross hack to get this done.
   365  //
   366  // TODO(shyamjvs): Make this work with multizonal and regional clusters.
   367  func (g *gkeDeployer) DumpClusterLogs(localPath, gcsPath string) error {
   368  	// gkeLogDumpTemplate is a template of a shell script where
   369  	// - %[1]s is the project
   370  	// - %[2]s is the zone
   371  	// - %[3]s is a filter composed of the instance groups
   372  	// - %[4]s is the log-dump.sh command line
   373  	const gkeLogDumpTemplate = `
   374  function log_dump_custom_get_instances() {
   375    if [[ $1 == "master" ]]; then
   376      return 0
   377    fi
   378  
   379    gcloud compute instances list '--project=%[1]s' '--filter=%[4]s' '--format=get(name)'
   380  }
   381  export -f log_dump_custom_get_instances
   382  # Set below vars that log-dump.sh expects in order to use scp with gcloud.
   383  export PROJECT=%[1]s
   384  export ZONE='%[2]s'
   385  export KUBERNETES_PROVIDER=gke
   386  export KUBE_NODE_OS_DISTRIBUTION='%[3]s'
   387  %[5]s
   388  `
   389  	// Prevent an obvious injection.
   390  	if strings.Contains(localPath, "'") || strings.Contains(gcsPath, "'") {
   391  		return fmt.Errorf("%q or %q contain single quotes - nice try", localPath, gcsPath)
   392  	}
   393  
   394  	// Generate a slice of filters to be OR'd together below
   395  	if err := g.getInstanceGroups(); err != nil {
   396  		return err
   397  	}
   398  	var filters []string
   399  	for _, ig := range g.instanceGroups {
   400  		filters = append(filters, fmt.Sprintf("(metadata.created-by:*%s)", ig.path))
   401  	}
   402  
   403  	// Generate the log-dump.sh command-line
   404  	var dumpCmd string
   405  	if gcsPath == "" {
   406  		dumpCmd = fmt.Sprintf("./cluster/log-dump/log-dump.sh '%s'", localPath)
   407  	} else {
   408  		dumpCmd = fmt.Sprintf("./cluster/log-dump/log-dump.sh '%s' '%s'", localPath, gcsPath)
   409  	}
   410  	return control.FinishRunning(exec.Command("bash", "-c", fmt.Sprintf(gkeLogDumpTemplate,
   411  		g.project,
   412  		g.zone,
   413  		os.Getenv("NODE_OS_DISTRIBUTION"),
   414  		strings.Join(filters, " OR "),
   415  		dumpCmd)))
   416  }
   417  
   418  func (g *gkeDeployer) TestSetup() error {
   419  	if g.setup {
   420  		// Ensure setup is a singleton.
   421  		return nil
   422  	}
   423  	if err := g.getKubeConfig(); err != nil {
   424  		return err
   425  	}
   426  	if err := g.getInstanceGroups(); err != nil {
   427  		return err
   428  	}
   429  	if err := g.ensureFirewall(); err != nil {
   430  		return err
   431  	}
   432  	if err := g.setupEnv(); err != nil {
   433  		return err
   434  	}
   435  	g.setup = true
   436  	return nil
   437  }
   438  
   439  func (g *gkeDeployer) getKubeConfig() error {
   440  	info, err := os.Stat(g.kubecfg)
   441  	if err != nil {
   442  		return err
   443  	}
   444  	if info.Size() > 0 {
   445  		// Assume that if we already have it, it's good.
   446  		return nil
   447  	}
   448  	if err := os.Setenv("KUBECONFIG", g.kubecfg); err != nil {
   449  		return err
   450  	}
   451  	if err := control.FinishRunning(exec.Command("gcloud", g.containerArgs("clusters", "get-credentials", g.cluster,
   452  		"--project="+g.project,
   453  		g.location)...)); err != nil {
   454  		return fmt.Errorf("error executing get-credentials: %v", err)
   455  	}
   456  	return nil
   457  }
   458  
   459  // setupEnv is to appease ginkgo-e2e.sh and other pieces of the e2e infrastructure. It
   460  // would be nice to handle this elsewhere, and not with env
   461  // variables. c.f. kubernetes/test-infra#3330.
   462  func (g *gkeDeployer) setupEnv() error {
   463  	// If singleZoneNodeInstanceGroup is true, set NODE_INSTANCE_GROUP to the
   464  	// names of instance groups that are in the same zone as the lexically first
   465  	// instance group. Otherwise set NODE_INSTANCE_GROUP to the names of all
   466  	// instance groups.
   467  	var filt []string
   468  	zone := g.instanceGroups[0].zone
   469  	for _, ig := range g.instanceGroups {
   470  		if !g.singleZoneNodeInstanceGroup || ig.zone == zone {
   471  			filt = append(filt, ig.name)
   472  		}
   473  	}
   474  	if err := os.Setenv("NODE_INSTANCE_GROUP", strings.Join(filt, ",")); err != nil {
   475  		return fmt.Errorf("error setting NODE_INSTANCE_GROUP: %v", err)
   476  	}
   477  	return nil
   478  }
   479  
   480  func (g *gkeDeployer) ensureFirewall() error {
   481  	firewall, err := g.getClusterFirewall()
   482  	if err != nil {
   483  		return fmt.Errorf("error getting unique firewall: %v", err)
   484  	}
   485  	if control.NoOutput(exec.Command("gcloud", "compute", "firewall-rules", "describe", firewall,
   486  		"--project="+g.project,
   487  		"--format=value(name)")) == nil {
   488  		// Assume that if this unique firewall exists, it's good to go.
   489  		return nil
   490  	}
   491  	log.Printf("Couldn't describe firewall '%s', assuming it doesn't exist and creating it", firewall)
   492  
   493  	tagOut, err := exec.Command("gcloud", "compute", "instances", "list",
   494  		"--project="+g.project,
   495  		"--filter=metadata.created-by:*"+g.instanceGroups[0].path,
   496  		"--limit=1",
   497  		"--format=get(tags.items)").Output()
   498  	if err != nil {
   499  		return fmt.Errorf("instances list failed: %s", util.ExecError(err))
   500  	}
   501  	tag := strings.TrimSpace(string(tagOut))
   502  	if tag == "" {
   503  		return fmt.Errorf("instances list returned no instances (or instance has no tags)")
   504  	}
   505  
   506  	if err := control.FinishRunning(exec.Command("gcloud", "compute", "firewall-rules", "create", firewall,
   507  		"--project="+g.project,
   508  		"--network="+g.network,
   509  		"--allow="+e2eAllow,
   510  		"--target-tags="+tag)); err != nil {
   511  		return fmt.Errorf("error creating e2e firewall: %v", err)
   512  	}
   513  	return nil
   514  }
   515  
   516  func (g *gkeDeployer) getInstanceGroups() error {
   517  	if len(g.instanceGroups) > 0 {
   518  		return nil
   519  	}
   520  	igs, err := exec.Command("gcloud", g.containerArgs("clusters", "describe", g.cluster,
   521  		"--format=value(instanceGroupUrls)",
   522  		"--project="+g.project,
   523  		g.location)...).Output()
   524  	if err != nil {
   525  		return fmt.Errorf("instance group URL fetch failed: %s", util.ExecError(err))
   526  	}
   527  	igURLs := strings.Split(strings.TrimSpace(string(igs)), ";")
   528  	if len(igURLs) == 0 {
   529  		return fmt.Errorf("no instance group URLs returned by gcloud, output %q", string(igs))
   530  	}
   531  	sort.Strings(igURLs)
   532  	for _, igURL := range igURLs {
   533  		m := poolRe.FindStringSubmatch(igURL)
   534  		if len(m) == 0 {
   535  			return fmt.Errorf("instanceGroupUrl %q did not match regex %v", igURL, poolRe)
   536  		}
   537  		g.instanceGroups = append(g.instanceGroups, &ig{path: m[0], zone: m[1], name: m[2], uniq: m[3]})
   538  	}
   539  	return nil
   540  }
   541  
   542  func (g *gkeDeployer) getClusterFirewall() (string, error) {
   543  	if err := g.getInstanceGroups(); err != nil {
   544  		return "", err
   545  	}
   546  	// We want to ensure that there's an e2e-ports-* firewall rule
   547  	// that maps to the cluster nodes, but the target tag for the
   548  	// nodes can be slow to get. Use the hash from the lexically first
   549  	// node pool instead.
   550  	return "e2e-ports-" + g.instanceGroups[0].uniq, nil
   551  }
   552  
   553  // This function ensures that all firewall-rules are deleted from specific network.
   554  // We also want to keep in logs that there were some resources leaking.
   555  func (g *gkeDeployer) cleanupNetworkFirewalls() (int, error) {
   556  	fws, err := exec.Command("gcloud", "compute", "firewall-rules", "list",
   557  		"--format=value(name)",
   558  		"--project="+g.project,
   559  		"--filter=network:"+g.network).Output()
   560  	if err != nil {
   561  		return 0, fmt.Errorf("firewall rules list failed: %s", util.ExecError(err))
   562  	}
   563  	if len(fws) > 0 {
   564  		fwList := strings.Split(strings.TrimSpace(string(fws)), "\n")
   565  		log.Printf("Network %s has %v undeleted firewall rules %v", g.network, len(fwList), fwList)
   566  		commandArgs := []string{"compute", "firewall-rules", "delete", "-q"}
   567  		commandArgs = append(commandArgs, fwList...)
   568  		commandArgs = append(commandArgs, "--project="+g.project)
   569  		errFirewall := control.FinishRunning(exec.Command("gcloud", commandArgs...))
   570  		if errFirewall != nil {
   571  			return 0, fmt.Errorf("error deleting firewall: %v", errFirewall)
   572  		}
   573  		return len(fwList), nil
   574  	}
   575  	return 0, nil
   576  }
   577  
   578  func (g *gkeDeployer) Down() error {
   579  	firewall, err := g.getClusterFirewall()
   580  	if err != nil {
   581  		// This is expected if the cluster doesn't exist.
   582  		return nil
   583  	}
   584  	g.instanceGroups = nil
   585  
   586  	// We best-effort try all of these and report errors as appropriate.
   587  	errCluster := control.FinishRunning(exec.Command(
   588  		"gcloud", g.containerArgs("clusters", "delete", "-q", g.cluster,
   589  			"--project="+g.project,
   590  			g.location)...))
   591  	var errFirewall error
   592  	if control.NoOutput(exec.Command("gcloud", "compute", "firewall-rules", "describe", firewall,
   593  		"--project="+g.project,
   594  		"--format=value(name)")) == nil {
   595  		log.Printf("Found rules for firewall '%s', deleting them", firewall)
   596  		errFirewall = control.FinishRunning(exec.Command("gcloud", "compute", "firewall-rules", "delete", "-q", firewall,
   597  			"--project="+g.project))
   598  	} else {
   599  		log.Printf("Found no rules for firewall '%s', assuming resources are clean", firewall)
   600  	}
   601  	numLeakedFWRules, errCleanFirewalls := g.cleanupNetworkFirewalls()
   602  	var errSubnet error
   603  	if g.subnetwork != "" {
   604  		errSubnet = control.FinishRunning(exec.Command("gcloud", "compute", "networks", "subnets", "delete", "-q", g.subnetwork,
   605  			g.subnetworkRegion, "--project="+g.project))
   606  	}
   607  	errNetwork := control.FinishRunning(exec.Command("gcloud", "compute", "networks", "delete", "-q", g.network,
   608  		"--project="+g.project))
   609  	if errCluster != nil {
   610  		return fmt.Errorf("error deleting cluster: %v", errCluster)
   611  	}
   612  	if errFirewall != nil {
   613  		return fmt.Errorf("error deleting firewall: %v", errFirewall)
   614  	}
   615  	if errCleanFirewalls != nil {
   616  		return fmt.Errorf("error cleaning-up firewalls: %v", errCleanFirewalls)
   617  	}
   618  	if errSubnet != nil {
   619  		return fmt.Errorf("error deleting subnetwork: %v", errSubnet)
   620  	}
   621  	if errNetwork != nil {
   622  		return fmt.Errorf("error deleting network: %v", errNetwork)
   623  	}
   624  	if numLeakedFWRules > 0 {
   625  		return fmt.Errorf("leaked firewall rules")
   626  	}
   627  	return nil
   628  }
   629  
   630  func (g *gkeDeployer) containerArgs(args ...string) []string {
   631  	return append(append(append([]string{}, g.commandGroup...), "container"), args...)
   632  }
   633  
   634  func (g *gkeDeployer) GetClusterCreated(gcpProject string) (time.Time, error) {
   635  	res, err := control.Output(exec.Command(
   636  		"gcloud",
   637  		"compute",
   638  		"instance-groups",
   639  		"list",
   640  		"--project="+gcpProject,
   641  		"--format=json(name,creationTimestamp)"))
   642  	if err != nil {
   643  		return time.Time{}, fmt.Errorf("list instance-group failed : %v", err)
   644  	}
   645  
   646  	created, err := getLatestClusterUpTime(string(res))
   647  	if err != nil {
   648  		return time.Time{}, fmt.Errorf("parse time failed : got gcloud res %s, err %v", string(res), err)
   649  	}
   650  	return created, nil
   651  }