github.com/jenkins-x/test-infra@v0.0.7/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  	"errors"
    22  	"flag"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"log"
    26  	"os"
    27  	"os/exec"
    28  	"regexp"
    29  	"strings"
    30  	"text/template"
    31  	"time"
    32  )
    33  
    34  const defaultKubeadmCNI = "weave"
    35  
    36  var (
    37  	// kubernetes-anywhere specific flags.
    38  	kubernetesAnywherePath = flag.String("kubernetes-anywhere-path", "",
    39  		"(kubernetes-anywhere only) Path to the kubernetes-anywhere directory. Must be set for kubernetes-anywhere.")
    40  	kubernetesAnywherePhase2Provider = flag.String("kubernetes-anywhere-phase2-provider", "ignition",
    41  		"(kubernetes-anywhere only) Provider for phase2 bootstrapping. (Defaults to ignition).")
    42  	kubernetesAnywhereKubeadmVersion = flag.String("kubernetes-anywhere-kubeadm-version", "stable",
    43  		"(kubernetes-anywhere only) Version of kubeadm to use, if phase2-provider is kubeadm. May be \"stable\" or a gs:// link to a custom build.")
    44  	kubernetesAnywhereKubernetesVersion = flag.String("kubernetes-anywhere-kubernetes-version", "",
    45  		"(kubernetes-anywhere only) Version of Kubernetes to use (e.g. latest, stable, latest-1.6, 1.6.3, etc).")
    46  	kubernetesAnywhereKubeletVersion = flag.String("kubernetes-anywhere-kubelet-version", "stable",
    47  		"(kubernetes-anywhere only) Version of Kubelet to use, if phase2-provider is kubeadm. May be \"stable\" or a gs:// link to a custom build.")
    48  	kubernetesAnywhereKubeletCIVersion = flag.String("kubernetes-anywhere-kubelet-ci-version", "",
    49  		"(kubernetes-anywhere only) If specified, the ci version for the kubelet to use. Overrides kubernetes-anywhere-kubelet-version.")
    50  	kubernetesAnywhereCluster = flag.String("kubernetes-anywhere-cluster", "",
    51  		"(kubernetes-anywhere only) Cluster name. Must be set for kubernetes-anywhere.")
    52  	kubernetesAnywhereProxyMode = flag.String("kubernetes-anywhere-proxy-mode", "",
    53  		"(kubernetes-anywhere only) Chose kube-proxy mode.")
    54  	kubernetesAnywhereUpTimeout = flag.Duration("kubernetes-anywhere-up-timeout", 20*time.Minute,
    55  		"(kubernetes-anywhere only) Time limit between starting a cluster and making a successful call to the Kubernetes API.")
    56  	kubernetesAnywhereNumNodes = flag.Int("kubernetes-anywhere-num-nodes", 4,
    57  		"(kubernetes-anywhere only) Number of nodes to be deployed in the cluster.")
    58  	kubernetesAnywhereUpgradeMethod = flag.String("kubernetes-anywhere-upgrade-method", "upgrade",
    59  		"(kubernetes-anywhere only) Indicates whether to do the control plane upgrade with kubeadm method \"init\" or \"upgrade\"")
    60  	kubernetesAnywhereCNI = flag.String("kubernetes-anywhere-cni", "",
    61  		"(kubernetes-anywhere only) The name of the CNI plugin used for the cluster's SDN.")
    62  	kubernetesAnywhereDumpClusterLogs = flag.Bool("kubernetes-anywhere-dump-cluster-logs", true,
    63  		"(kubernetes-anywhere only) Whether to dump cluster logs.")
    64  	kubernetesAnywhereOSImage = flag.String("kubernetes-anywhere-os-image", "ubuntu-1604-xenial-v20171212",
    65  		"(kubernetes-anywhere only) The name of the os_image to use for nodes")
    66  	kubernetesAnywhereKubeadmFeatureGates = flag.String("kubernetes-anywhere-kubeadm-feature-gates", "",
    67  		"(kubernetes-anywhere only) A set of key=value pairs that describes feature gates for kubeadm features. If specified, this flag will pass on to kubeadm.")
    68  )
    69  
    70  const kubernetesAnywhereConfigTemplate = `
    71  .phase1.num_nodes={{.NumNodes}}
    72  .phase1.cluster_name="{{.Cluster}}"
    73  .phase1.ssh_user=""
    74  .phase1.cloud_provider="gce"
    75  
    76  .phase1.gce.os_image="{{.OSImage}}"
    77  .phase1.gce.instance_type="n1-standard-1"
    78  .phase1.gce.project="{{.Project}}"
    79  .phase1.gce.region="{{.Region}}"
    80  .phase1.gce.zone="{{.Zone}}"
    81  .phase1.gce.network="default"
    82  
    83  .phase2.installer_container="docker.io/colemickens/k8s-ignition:latest"
    84  .phase2.docker_registry="k8s.gcr.io"
    85  .phase2.kubernetes_version="{{.KubernetesVersion}}"
    86  .phase2.provider="{{.Phase2Provider}}"
    87  .phase2.kubelet_version="{{.KubeletVersion}}"
    88  .phase2.kubeadm.version="{{.KubeadmVersion}}"
    89  .phase2.kube_context_name="{{.KubeContext}}"
    90  .phase2.proxy_mode="{{.KubeproxyMode}}"
    91  .phase2.kubeadm.master_upgrade.method="{{.UpgradeMethod}}"
    92  .phase2.kubeadm.feature_gates="{{.KubeadmFeatureGates}}"
    93  
    94  .phase3.run_addons=y
    95  .phase3.kube_proxy=n
    96  .phase3.dashboard=n
    97  .phase3.heapster=n
    98  .phase3.kube_dns=n
    99  .phase3.cni="{{.CNI}}"
   100  `
   101  
   102  const kubernetesAnywhereMultiClusterConfigTemplate = kubernetesAnywhereConfigTemplate + `
   103  .phase2.enable_cloud_provider=y
   104  .phase3.gce_storage_class=y
   105  `
   106  
   107  type kubernetesAnywhere struct {
   108  	path string
   109  	// These are exported only because their use in the config template requires it.
   110  	Phase2Provider      string
   111  	KubeadmVersion      string
   112  	KubeletVersion      string
   113  	UpgradeMethod       string
   114  	KubernetesVersion   string
   115  	NumNodes            int
   116  	Project             string
   117  	Cluster             string
   118  	Zone                string
   119  	Region              string
   120  	KubeContext         string
   121  	CNI                 string
   122  	KubeproxyMode       string
   123  	OSImage             string
   124  	KubeadmFeatureGates string
   125  }
   126  
   127  func initializeKubernetesAnywhere(project, zone string) (*kubernetesAnywhere, error) {
   128  	if *kubernetesAnywherePath == "" {
   129  		return nil, fmt.Errorf("--kubernetes-anywhere-path is required")
   130  	}
   131  
   132  	if *kubernetesAnywhereCluster == "" {
   133  		return nil, fmt.Errorf("--kubernetes-anywhere-cluster is required")
   134  	}
   135  
   136  	if project == "" {
   137  		return nil, fmt.Errorf("--provider=kubernetes-anywhere requires --gcp-project")
   138  	}
   139  
   140  	if zone == "" {
   141  		zone = "us-central1-c"
   142  	}
   143  
   144  	kubeletVersion := *kubernetesAnywhereKubeletVersion
   145  	if *kubernetesAnywhereKubeletCIVersion != "" {
   146  		// resolvedVersion is EG v1.11.0-alpha.0.1031+d37460147ec956-bazel
   147  		resolvedVersion, err := resolveCIVersion(*kubernetesAnywhereKubeletCIVersion)
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  		kubeletVersion = fmt.Sprintf("gs://kubernetes-release-dev/ci/%v/bin/linux/amd64/", resolvedVersion)
   152  	}
   153  
   154  	// preserve backwards compatibility for e2e tests which never provided cni name
   155  	if *kubernetesAnywhereCNI == "" && *kubernetesAnywherePhase2Provider == "kubeadm" {
   156  		*kubernetesAnywhereCNI = defaultKubeadmCNI
   157  	}
   158  
   159  	k := &kubernetesAnywhere{
   160  		path:                *kubernetesAnywherePath,
   161  		Phase2Provider:      *kubernetesAnywherePhase2Provider,
   162  		KubeadmVersion:      *kubernetesAnywhereKubeadmVersion,
   163  		KubeletVersion:      kubeletVersion,
   164  		UpgradeMethod:       *kubernetesAnywhereUpgradeMethod,
   165  		KubernetesVersion:   *kubernetesAnywhereKubernetesVersion,
   166  		NumNodes:            *kubernetesAnywhereNumNodes,
   167  		Project:             project,
   168  		Cluster:             *kubernetesAnywhereCluster,
   169  		Zone:                zone,
   170  		Region:              regexp.MustCompile(`-[^-]+$`).ReplaceAllString(zone, ""),
   171  		CNI:                 *kubernetesAnywhereCNI,
   172  		KubeproxyMode:       *kubernetesAnywhereProxyMode,
   173  		OSImage:             *kubernetesAnywhereOSImage,
   174  		KubeadmFeatureGates: *kubernetesAnywhereKubeadmFeatureGates,
   175  	}
   176  
   177  	return k, nil
   178  }
   179  
   180  func newKubernetesAnywhere(project, zone string) (deployer, error) {
   181  	k, err := initializeKubernetesAnywhere(project, zone)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	// Set KUBERNETES_CONFORMANCE_TEST so the auth info is picked up
   187  	// from kubectl instead of bash inference.
   188  	if err := os.Setenv("KUBERNETES_CONFORMANCE_TEST", "yes"); err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	// Set KUBERNETES_CONFORMANCE_PROVIDER since KUBERNETES_CONFORMANCE_TEST is set
   193  	// to ensure the right provider is passed onto the test.
   194  	if err := os.Setenv("KUBERNETES_CONFORMANCE_PROVIDER", "kubernetes-anywhere"); err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	if err := k.writeConfig(kubernetesAnywhereConfigTemplate); err != nil {
   199  		return nil, err
   200  	}
   201  	return k, nil
   202  }
   203  
   204  func resolveCIVersion(version string) (string, error) {
   205  	file := fmt.Sprintf("gs://kubernetes-release-dev/ci/%v.txt", version)
   206  	return readGSFile(file)
   207  }
   208  
   209  // Implemented as a function var for testing.
   210  var readGSFile = readGSFileImpl
   211  
   212  func readGSFileImpl(filepath string) (string, error) {
   213  	contents, err := control.Output(exec.Command("gsutil", "cat", filepath))
   214  	if err != nil {
   215  		return "", err
   216  	}
   217  	return strings.TrimSpace(string(contents)), nil
   218  }
   219  
   220  func (k *kubernetesAnywhere) getConfig(configTemplate string) ([]byte, error) {
   221  	// As needed, plumb through more CLI options to replace these defaults
   222  	tmpl, err := template.New("kubernetes-anywhere-config").Parse(configTemplate)
   223  	if err != nil {
   224  		return nil, fmt.Errorf("Error creating template for KubernetesAnywhere config: %v", err)
   225  	}
   226  
   227  	var buf bytes.Buffer
   228  	if err = tmpl.Execute(&buf, k); err != nil {
   229  		return nil, fmt.Errorf("Error executing template for KubernetesAnywhere config: %v", err)
   230  	}
   231  
   232  	return buf.Bytes(), nil
   233  }
   234  
   235  func (k *kubernetesAnywhere) writeConfig(configTemplate string) error {
   236  	config, err := k.getConfig(configTemplate)
   237  	if err != nil {
   238  		return fmt.Errorf("Could not generate config: %v", err)
   239  	}
   240  	return ioutil.WriteFile(k.path+"/.config", config, 0644)
   241  }
   242  
   243  func (k *kubernetesAnywhere) Up() error {
   244  	cmd := exec.Command("make", "-C", k.path, "WAIT_FOR_KUBECONFIG=y", "deploy")
   245  	if err := control.FinishRunning(cmd); err != nil {
   246  		return err
   247  	}
   248  
   249  	if err := k.TestSetup(); err != nil {
   250  		return err
   251  	}
   252  
   253  	return waitForReadyNodes(k.NumNodes+1, *kubernetesAnywhereUpTimeout, 1)
   254  }
   255  
   256  func (k *kubernetesAnywhere) IsUp() error {
   257  	return isUp(k)
   258  }
   259  
   260  func (k *kubernetesAnywhere) DumpClusterLogs(localPath, gcsPath string) error {
   261  	if !*kubernetesAnywhereDumpClusterLogs {
   262  		log.Printf("Cluster log dumping disabled for Kubernetes Anywhere.")
   263  		return nil
   264  	}
   265  	return defaultDumpClusterLogs(localPath, gcsPath)
   266  }
   267  
   268  func (k *kubernetesAnywhere) TestSetup() error {
   269  	o, err := control.Output(exec.Command("make", "--silent", "-C", k.path, "kubeconfig-path"))
   270  	if err != nil {
   271  		return fmt.Errorf("Could not get kubeconfig-path: %v", err)
   272  	}
   273  	kubecfg := strings.TrimSuffix(string(o), "\n")
   274  
   275  	if err = os.Setenv("KUBECONFIG", kubecfg); err != nil {
   276  		return err
   277  	}
   278  	return nil
   279  }
   280  
   281  func (k *kubernetesAnywhere) Down() error {
   282  	err := control.FinishRunning(exec.Command("make", "-C", k.path, "kubeconfig-path"))
   283  	if err != nil {
   284  		// This is expected if the cluster doesn't exist.
   285  		return nil
   286  	}
   287  	return control.FinishRunning(exec.Command("make", "-C", k.path, "FORCE_DESTROY=y", "destroy"))
   288  }
   289  
   290  func (k *kubernetesAnywhere) GetClusterCreated(gcpProject string) (time.Time, error) {
   291  	return time.Time{}, errors.New("not implemented")
   292  }
   293  
   294  func (_ *kubernetesAnywhere) KubectlCommand() (*exec.Cmd, error) { return nil, nil }
   295  
   296  const defaultConfigFile = ".config"
   297  
   298  type kubernetesAnywhereMultiCluster struct {
   299  	*kubernetesAnywhere
   300  	multiClusters  multiClusterDeployment
   301  	configFile     map[string]string
   302  	kubeContextMap map[string]string
   303  }
   304  
   305  // newKubernetesAnywhereMultiCluster returns the deployer based on kubernetes-anywhere
   306  // which can be used to deploy multiple clusters simultaneously.
   307  func newKubernetesAnywhereMultiCluster(project, zone string, multiClusters multiClusterDeployment) (deployer, error) {
   308  	if len(multiClusters.clusters) < 1 {
   309  		return nil, fmt.Errorf("invalid --multi-clusters flag passed")
   310  	}
   311  	k, err := initializeKubernetesAnywhere(project, zone)
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  	mk := &kubernetesAnywhereMultiCluster{k, multiClusters, make(map[string]string), make(map[string]string)}
   316  
   317  	for _, cluster := range mk.multiClusters.clusters {
   318  		specificZone, specified := mk.multiClusters.zones[cluster]
   319  		if specified {
   320  			mk.Zone = specificZone
   321  		}
   322  		mk.Cluster = cluster
   323  		// TODO: revisit the naming of kubecontexts. Currently the federation CI jobs require that the
   324  		// cluster contexts be prefixed with `federation-` and with particular pattern.
   325  		mk.KubeContext = "federation-e2e-gce-" + mk.Zone
   326  		mk.kubeContextMap[cluster] = mk.KubeContext
   327  		mk.configFile[cluster] = defaultConfigFile + "-" + mk.Cluster
   328  		if err := mk.writeConfig(kubernetesAnywhereMultiClusterConfigTemplate); err != nil {
   329  			return nil, err
   330  		}
   331  	}
   332  	return mk, nil
   333  }
   334  
   335  // writeConfig writes the kubernetes-anywhere config file to file system after
   336  // rendering the template file with configuration in deployer.
   337  func (k *kubernetesAnywhereMultiCluster) writeConfig(configTemplate string) error {
   338  	config, err := k.getConfig(configTemplate)
   339  	if err != nil {
   340  		return fmt.Errorf("could not generate config: %v", err)
   341  	}
   342  
   343  	return ioutil.WriteFile(k.path+"/"+k.configFile[k.Cluster], config, 0644)
   344  }
   345  
   346  // Up brings up multiple k8s clusters in parallel.
   347  func (k *kubernetesAnywhereMultiCluster) Up() error {
   348  	var cmds []*exec.Cmd
   349  	for _, cluster := range k.multiClusters.clusters {
   350  		cmd := exec.Command("make", "-C", k.path, "CONFIG_FILE="+k.configFile[cluster], "deploy")
   351  		cmds = append(cmds, cmd)
   352  	}
   353  
   354  	if err := control.FinishRunningParallel(cmds...); err != nil {
   355  		return err
   356  	}
   357  
   358  	return k.TestSetup()
   359  }
   360  
   361  // TestSetup sets up test environment by merging kubeconfig of multiple deployments.
   362  func (k *kubernetesAnywhereMultiCluster) TestSetup() error {
   363  	mergedKubeconfigPath := k.path + "/kubeconfig.json"
   364  	var kubecfg string
   365  	for _, cluster := range k.multiClusters.clusters {
   366  		o, err := control.Output(exec.Command("make", "--silent", "-C", k.path, "CONFIG_FILE="+k.configFile[cluster], "kubeconfig-path"))
   367  		if err != nil {
   368  			return fmt.Errorf("could not get kubeconfig-path: %v", err)
   369  		}
   370  		if len(kubecfg) != 0 {
   371  			kubecfg += ":"
   372  		}
   373  		kubecfg += strings.TrimSuffix(string(o), "\n")
   374  	}
   375  	if len(kubecfg) != 0 {
   376  		kubecfg += ":" + mergedKubeconfigPath
   377  	}
   378  
   379  	if err := os.Setenv("KUBECONFIG", kubecfg); err != nil {
   380  		return err
   381  	}
   382  
   383  	o, err := control.Output(exec.Command("kubectl", "config", "view", "--flatten=true", "--raw=true"))
   384  	if err != nil {
   385  		return fmt.Errorf("could not get kubeconfig-path: %v", err)
   386  	}
   387  
   388  	err = ioutil.WriteFile(mergedKubeconfigPath, o, 0644)
   389  	if err != nil {
   390  		return err
   391  	}
   392  
   393  	return os.Setenv("KUBECONFIG", mergedKubeconfigPath)
   394  }
   395  
   396  // IsUp checks if all the clusters in the deployer are up.
   397  func (k *kubernetesAnywhereMultiCluster) IsUp() error {
   398  	if err := k.TestSetup(); err != nil {
   399  		return err
   400  	}
   401  
   402  	for _, cluster := range k.multiClusters.clusters {
   403  		kubeContext := k.kubeContextMap[cluster]
   404  		o, err := control.Output(exec.Command("kubectl", "--context="+kubeContext, "get", "nodes", "--no-headers"))
   405  		if err != nil {
   406  			log.Printf("kubectl get nodes failed for cluster %s: %s\n%s", cluster, wrapError(err).Error(), string(o))
   407  			return err
   408  		}
   409  		stdout := strings.TrimSpace(string(o))
   410  		log.Printf("Cluster nodes of cluster %s:\n%s", cluster, stdout)
   411  
   412  		n := len(strings.Split(stdout, "\n"))
   413  		if n < k.NumNodes {
   414  			return fmt.Errorf("cluster %s found, but %d nodes reported", cluster, n)
   415  		}
   416  	}
   417  	return nil
   418  }
   419  
   420  // Down brings down multiple k8s clusters in parallel.
   421  func (k *kubernetesAnywhereMultiCluster) Down() error {
   422  	if err := k.TestSetup(); err != nil {
   423  		// This is expected if the clusters doesn't exist.
   424  		return nil
   425  	}
   426  
   427  	var cmds []*exec.Cmd
   428  	for _, cluster := range k.multiClusters.clusters {
   429  		cmd := exec.Command("make", "-C", k.path, "CONFIG_FILE="+k.configFile[cluster], "FORCE_DESTROY=y", "destroy")
   430  		cmds = append(cmds, cmd)
   431  	}
   432  	return control.FinishRunningParallel(cmds...)
   433  }
   434  
   435  func (_ *kubernetesAnywhereMultiCluster) KubectlCommand() (*exec.Cmd, error) { return nil, nil }