github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/kubetest/anywhere.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
    18  
    19  import (
    20  	"bytes"
    21  	"flag"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"log"
    25  	"os"
    26  	"os/exec"
    27  	"regexp"
    28  	"strings"
    29  	"text/template"
    30  	"time"
    31  )
    32  
    33  var (
    34  	// kubernetes-anywhere specific flags.
    35  	kubernetesAnywherePath = flag.String("kubernetes-anywhere-path", "",
    36  		"(kubernetes-anywhere only) Path to the kubernetes-anywhere directory. Must be set for kubernetes-anywhere.")
    37  	kubernetesAnywherePhase2Provider = flag.String("kubernetes-anywhere-phase2-provider", "ignition",
    38  		"(kubernetes-anywhere only) Provider for phase2 bootstrapping. (Defaults to ignition).")
    39  	kubernetesAnywhereKubeadmVersion = flag.String("kubernetes-anywhere-kubeadm-version", "stable",
    40  		"(kubernetes-anywhere only) Version of kubeadm to use, if phase2-provider is kubeadm. May be \"stable\" or a gs:// link to a custom build.")
    41  	kubernetesAnywhereKubernetesVersion = flag.String("kubernetes-anywhere-kubernetes-version", "",
    42  		"(kubernetes-anywhere only) Version of Kubernetes to use (e.g. latest, stable, latest-1.6, 1.6.3, etc).")
    43  	kubernetesAnywhereKubeletVersion = flag.String("kubernetes-anywhere-kubelet-version", "stable",
    44  		"(kubernetes-anywhere only) Version of Kubelet to use, if phase2-provider is kubeadm. May be \"stable\" or a gs:// link to a custom build.")
    45  	kubernetesAnywhereKubeletCIVersion = flag.String("kubernetes-anywhere-kubelet-ci-version", "",
    46  		"(kubernetes-anywhere only) If specified, the ci version for the kubelt to use. Overrides kubernetes-anywhere-kubelet-version.")
    47  	kubernetesAnywhereCluster = flag.String("kubernetes-anywhere-cluster", "",
    48  		"(kubernetes-anywhere only) Cluster name. Must be set for kubernetes-anywhere.")
    49  	kubernetesAnywhereUpTimeout = flag.Duration("kubernetes-anywhere-up-timeout", 20*time.Minute,
    50  		"(kubernetes-anywhere only) Time limit between starting a cluster and making a successful call to the Kubernetes API.")
    51  	kubernetesAnywhereNumNodes = flag.Int("kubernetes-anywhere-num-nodes", 4,
    52  		"(kubernetes-anywhere only) Number of nodes to be deployed in the cluster.")
    53  	kubernetesAnywhereUpgradeMethod = flag.String("kubernetes-anywhere-upgrade-method", "upgrade",
    54  		"(kubernetes-anywhere only) Indicates whether to do the control plane upgrade with kubeadm method \"init\" or \"upgrade\"")
    55  )
    56  
    57  const kubernetesAnywhereConfigTemplate = `
    58  .phase1.num_nodes={{.NumNodes}}
    59  .phase1.cluster_name="{{.Cluster}}"
    60  .phase1.ssh_user=""
    61  .phase1.cloud_provider="gce"
    62  
    63  .phase1.gce.os_image="ubuntu-1604-xenial-v20160420c"
    64  .phase1.gce.instance_type="n1-standard-1"
    65  .phase1.gce.project="{{.Project}}"
    66  .phase1.gce.region="{{.Region}}"
    67  .phase1.gce.zone="{{.Zone}}"
    68  .phase1.gce.network="default"
    69  
    70  .phase2.installer_container="docker.io/colemickens/k8s-ignition:latest"
    71  .phase2.docker_registry="gcr.io/google-containers"
    72  .phase2.kubernetes_version="{{.KubernetesVersion}}"
    73  .phase2.provider="{{.Phase2Provider}}"
    74  .phase2.kubelet_version="{{.KubeletVersion}}"
    75  .phase2.kubeadm.version="{{.KubeadmVersion}}"
    76  .phase2.kube_context_name="{{.KubeContext}}"
    77  .phase2.kubeadm.master_upgrade.method="{{.UpgradeMethod}}"
    78  
    79  .phase3.run_addons=y
    80  .phase3.weave_net={{if eq .Phase2Provider "kubeadm" -}} y {{- else -}} n {{- end}}
    81  .phase3.kube_proxy=n
    82  .phase3.dashboard=n
    83  .phase3.heapster=n
    84  .phase3.kube_dns=n
    85  `
    86  
    87  type kubernetesAnywhere struct {
    88  	path string
    89  	// These are exported only because their use in the config template requires it.
    90  	Phase2Provider    string
    91  	KubeadmVersion    string
    92  	KubeletVersion    string
    93  	UpgradeMethod     string
    94  	KubernetesVersion string
    95  	NumNodes          int
    96  	Project           string
    97  	Cluster           string
    98  	Zone              string
    99  	Region            string
   100  	KubeContext       string
   101  }
   102  
   103  func newKubernetesAnywhere(project, zone string) (deployer, error) {
   104  	if *kubernetesAnywherePath == "" {
   105  		return nil, fmt.Errorf("--kubernetes-anywhere-path is required")
   106  	}
   107  
   108  	if *kubernetesAnywhereCluster == "" {
   109  		return nil, fmt.Errorf("--kubernetes-anywhere-cluster is required")
   110  	}
   111  
   112  	if project == "" {
   113  		return nil, fmt.Errorf("--provider=kubernetes-anywhere requires --gcp-project")
   114  	}
   115  
   116  	if zone == "" {
   117  		zone = "us-central1-c"
   118  	}
   119  
   120  	kubeletVersion := *kubernetesAnywhereKubeletVersion
   121  	if *kubernetesAnywhereKubeletCIVersion != "" {
   122  		resolvedVersion, err := resolveCIVersion(*kubernetesAnywhereKubeletCIVersion)
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  		kubeletVersion = bazelBuildPath(resolvedVersion)
   127  	}
   128  
   129  	// Set KUBERNETES_CONFORMANCE_TEST so the auth info is picked up
   130  	// from kubectl instead of bash inference.
   131  	if err := os.Setenv("KUBERNETES_CONFORMANCE_TEST", "yes"); err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	// Set KUBERNETES_CONFORMANCE_PROVIDER since KUBERNETES_CONFORMANCE_TEST is set
   136  	// to ensure the right provider is passed onto the test.
   137  	if err := os.Setenv("KUBERNETES_CONFORMANCE_PROVIDER", "kubernetes-anywhere"); err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	k := &kubernetesAnywhere{
   142  		path:              *kubernetesAnywherePath,
   143  		Phase2Provider:    *kubernetesAnywherePhase2Provider,
   144  		KubeadmVersion:    *kubernetesAnywhereKubeadmVersion,
   145  		KubeletVersion:    kubeletVersion,
   146  		UpgradeMethod:     *kubernetesAnywhereUpgradeMethod,
   147  		KubernetesVersion: *kubernetesAnywhereKubernetesVersion,
   148  		NumNodes:          *kubernetesAnywhereNumNodes,
   149  		Project:           project,
   150  		Cluster:           *kubernetesAnywhereCluster,
   151  		Zone:              zone,
   152  		Region:            regexp.MustCompile(`-[^-]+$`).ReplaceAllString(zone, ""),
   153  	}
   154  
   155  	if err := k.writeConfig(); err != nil {
   156  		return nil, err
   157  	}
   158  	return k, nil
   159  }
   160  
   161  func resolveCIVersion(version string) (string, error) {
   162  	if strings.HasPrefix(version, "v") {
   163  		return version, nil
   164  	}
   165  	file := fmt.Sprintf("gs://kubernetes-release-dev/ci/%v.txt", version)
   166  	return readGSFile(file)
   167  }
   168  
   169  func bazelBuildPath(version string) string {
   170  	// This replicates the logic from scenarios/kubernetes_e2e.py, to
   171  	// accommodate the fact that bazel artifacts are stored in a different
   172  	// location for 1.6 builds.
   173  	if strings.HasPrefix(version, "v1.6.") {
   174  		return fmt.Sprintf("gs://kubernetes-release-dev/bazel/%v/build/debs/", version)
   175  	} else {
   176  		return fmt.Sprintf("gs://kubernetes-release-dev/bazel/%v/bin/linux/amd64/", version)
   177  	}
   178  }
   179  
   180  // Implemented as a function var for testing.
   181  var readGSFile = readGSFileImpl
   182  
   183  func readGSFileImpl(filepath string) (string, error) {
   184  	contents, err := output(exec.Command("gsutil", "cat", filepath))
   185  	if err != nil {
   186  		return "", err
   187  	}
   188  	return strings.TrimSpace(string(contents)), nil
   189  }
   190  
   191  func (k *kubernetesAnywhere) getConfig() ([]byte, error) {
   192  	// As needed, plumb through more CLI options to replace these defaults
   193  	tmpl, err := template.New("kubernetes-anywhere-config").Parse(kubernetesAnywhereConfigTemplate)
   194  	if err != nil {
   195  		return nil, fmt.Errorf("Error creating template for KubernetesAnywhere config: %v", err)
   196  	}
   197  
   198  	var buf bytes.Buffer
   199  	if err = tmpl.Execute(&buf, k); err != nil {
   200  		return nil, fmt.Errorf("Error executing template for KubernetesAnywhere config: %v", err)
   201  	}
   202  
   203  	return buf.Bytes(), nil
   204  }
   205  
   206  func (k *kubernetesAnywhere) writeConfig() error {
   207  	config, err := k.getConfig()
   208  	if err != nil {
   209  		return fmt.Errorf("Could not generate config: %v", err)
   210  	}
   211  	return ioutil.WriteFile(k.path+"/.config", config, 0644)
   212  }
   213  
   214  func (k *kubernetesAnywhere) Up() error {
   215  	cmd := exec.Command("make", "-C", k.path, "WAIT_FOR_KUBECONFIG=y", "deploy")
   216  	if err := finishRunning(cmd); err != nil {
   217  		return err
   218  	}
   219  
   220  	nodes := k.NumNodes
   221  	return waitForNodes(k, nodes+1, *kubernetesAnywhereUpTimeout)
   222  }
   223  
   224  func (k *kubernetesAnywhere) IsUp() error {
   225  	return isUp(k)
   226  }
   227  
   228  func (k *kubernetesAnywhere) DumpClusterLogs(localPath, gcsPath string) error {
   229  	// TODO(pipejakob): the default implementation (log-dump.sh) doesn't work for
   230  	// kubernetes-anywhere yet, so just skip attempting to dump logs.
   231  	// https://github.com/kubernetes/kubeadm/issues/256
   232  	log.Print("DumpClusterLogs is a no-op for kubernetes-anywhere deployments. Not doing anything.")
   233  	log.Print("If you care about enabling this feature, follow this issue for progress:")
   234  	log.Print("    https://github.com/kubernetes/kubeadm/issues/256")
   235  	return nil
   236  }
   237  
   238  func (k *kubernetesAnywhere) TestSetup() error {
   239  	o, err := output(exec.Command("make", "--silent", "-C", k.path, "kubeconfig-path"))
   240  	if err != nil {
   241  		return fmt.Errorf("Could not get kubeconfig-path: %v", err)
   242  	}
   243  	kubecfg := strings.TrimSuffix(string(o), "\n")
   244  
   245  	if err = os.Setenv("KUBECONFIG", kubecfg); err != nil {
   246  		return err
   247  	}
   248  	return nil
   249  }
   250  
   251  func (k *kubernetesAnywhere) Down() error {
   252  	err := finishRunning(exec.Command("make", "-C", k.path, "kubeconfig-path"))
   253  	if err != nil {
   254  		// This is expected if the cluster doesn't exist.
   255  		return nil
   256  	}
   257  	return finishRunning(exec.Command("make", "-C", k.path, "FORCE_DESTROY=y", "destroy"))
   258  }
   259  
   260  const defaultConfigFile = ".config"
   261  
   262  type kubernetesAnywhereMultiCluster struct {
   263  	*kubernetesAnywhere
   264  	multiClusters multiClusterDeployment
   265  	configFile    map[string]string
   266  }
   267  
   268  // newKubernetesAnywhereMultiCluster returns the deployer based on kubernetes-anywhere
   269  // which can be used to deploy multiple clusters simultaneously.
   270  func newKubernetesAnywhereMultiCluster(project, zone string, multiClusters multiClusterDeployment) (deployer, error) {
   271  	if len(multiClusters.clusters) < 1 {
   272  		return nil, fmt.Errorf("invalid --multi-clusters flag passed")
   273  	}
   274  	k, err := newKubernetesAnywhere(project, zone)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	mk := &kubernetesAnywhereMultiCluster{k.(*kubernetesAnywhere), multiClusters, make(map[string]string)}
   279  
   280  	for _, cluster := range mk.multiClusters.clusters {
   281  		specificZone, specified := mk.multiClusters.zones[cluster]
   282  		if specified {
   283  			mk.Zone = specificZone
   284  		}
   285  		mk.Cluster = cluster
   286  		mk.KubeContext = mk.Zone + "-" + mk.Cluster
   287  		mk.configFile[cluster] = defaultConfigFile + "-" + mk.Cluster
   288  		if err := mk.writeConfig(); err != nil {
   289  			return nil, err
   290  		}
   291  	}
   292  	return mk, nil
   293  }
   294  
   295  // writeConfig writes the kubernetes-anywhere config file to file system after
   296  // rendering the template file with configuration in deployer.
   297  func (k *kubernetesAnywhereMultiCluster) writeConfig() error {
   298  	config, err := k.getConfig()
   299  	if err != nil {
   300  		return fmt.Errorf("could not generate config: %v", err)
   301  	}
   302  
   303  	return ioutil.WriteFile(k.path+"/"+k.configFile[k.Cluster], config, 0644)
   304  }
   305  
   306  // Up brings up multiple k8s clusters in parallel.
   307  func (k *kubernetesAnywhereMultiCluster) Up() error {
   308  	var cmds []*exec.Cmd
   309  	for _, cluster := range k.multiClusters.clusters {
   310  		cmd := exec.Command("make", "-C", k.path, "CONFIG_FILE="+k.configFile[cluster], "deploy")
   311  		cmds = append(cmds, cmd)
   312  	}
   313  
   314  	if err := finishRunningParallel(cmds...); err != nil {
   315  		return err
   316  	}
   317  
   318  	return k.TestSetup()
   319  }
   320  
   321  // TestSetup sets up test environment by merging kubeconfig of multiple deployments.
   322  func (k *kubernetesAnywhereMultiCluster) TestSetup() error {
   323  	var kubecfg string
   324  	for _, cluster := range k.multiClusters.clusters {
   325  		o, err := output(exec.Command("make", "--silent", "-C", k.path, "CONFIG_FILE="+k.configFile[cluster], "kubeconfig-path"))
   326  		if err != nil {
   327  			return fmt.Errorf("could not get kubeconfig-path: %v", err)
   328  		}
   329  		if len(kubecfg) != 0 {
   330  			kubecfg += ":"
   331  		}
   332  		kubecfg += strings.TrimSuffix(string(o), "\n")
   333  	}
   334  
   335  	if err := os.Setenv("KUBECONFIG", kubecfg); err != nil {
   336  		return err
   337  	}
   338  	return nil
   339  }
   340  
   341  // IsUp checks if all the clusters in the deployer are up.
   342  func (k *kubernetesAnywhereMultiCluster) IsUp() error {
   343  	if err := k.TestSetup(); err != nil {
   344  		return err
   345  	}
   346  
   347  	for _, cluster := range k.multiClusters.clusters {
   348  		zone := k.multiClusters.zones[cluster]
   349  		kubeContext := zone + "-" + cluster
   350  		o, err := output(exec.Command("kubectl", "--context="+kubeContext, "get", "nodes", "--no-headers"))
   351  		if err != nil {
   352  			log.Printf("kubectl get nodes failed for cluster %s: %s\n%s", cluster, wrapError(err).Error(), string(o))
   353  			return err
   354  		}
   355  		stdout := strings.TrimSpace(string(o))
   356  		log.Printf("Cluster nodes of cluster %s:\n%s", cluster, stdout)
   357  
   358  		n := len(strings.Split(stdout, "\n"))
   359  		if n < k.NumNodes {
   360  			return fmt.Errorf("cluster %s found, but %d nodes reported", cluster, n)
   361  		}
   362  	}
   363  	return nil
   364  }
   365  
   366  // Down brings down multiple k8s clusters in parallel.
   367  func (k *kubernetesAnywhereMultiCluster) Down() error {
   368  	if err := k.TestSetup(); err != nil {
   369  		// This is expected if the clusters doesn't exist.
   370  		return nil
   371  	}
   372  
   373  	var cmds []*exec.Cmd
   374  	for _, cluster := range k.multiClusters.clusters {
   375  		cmd := exec.Command("make", "-C", k.path, "CONFIG_FILE="+k.configFile[cluster], "FORCE_DESTROY=y", "destroy")
   376  		cmds = append(cmds, cmd)
   377  	}
   378  	return finishRunningParallel(cmds...)
   379  }