github.com/maxgio92/test-infra@v0.1.0/kubetest/kind/kind.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package kind
    18  
    19  import (
    20  	"crypto/sha256"
    21  	"encoding/hex"
    22  	"errors"
    23  	"flag"
    24  	"fmt"
    25  	"io"
    26  	"log"
    27  	"net/http"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"runtime"
    32  	"strings"
    33  	"time"
    34  
    35  	"github.com/maxgio92/test-infra/kubetest/process"
    36  )
    37  
    38  const (
    39  	// note: this is under the user's home
    40  	kindBinarySubDir    = ".kubetest/kind"
    41  	kindNodeImageLatest = "kindest/node:latest"
    42  
    43  	kindBinaryBuild  = "build"
    44  	kindBinaryStable = "stable"
    45  
    46  	// If a new version of kind is released this value has to be updated.
    47  	kindBinaryStableTag = "v0.7.0"
    48  
    49  	kindClusterNameDefault = "kind-kubetest"
    50  
    51  	flagLogLevel = "--verbosity=9"
    52  )
    53  
    54  var (
    55  	kindConfigPath = flag.String("kind-config-path", "",
    56  		"(kind only) Path to the kind configuration file.")
    57  	kindKubeconfigPath = flag.String("kind-kubeconfig-path", "",
    58  		"(kind only) Path to the kubeconfig file for kind create cluster command.")
    59  	kindBaseImage = flag.String("kind-base-image", "",
    60  		"(kind only) name:tag of the base image to use for building the node image for kind.")
    61  	kindBinaryVersion = flag.String("kind-binary-version", kindBinaryStable,
    62  		fmt.Sprintf("(kind only) This flag can be either %q (build from source) "+
    63  			"or %q (download a stable binary).", kindBinaryBuild, kindBinaryStable))
    64  	kindClusterName = flag.String("kind-cluster-name", kindClusterNameDefault,
    65  		"(kind only) Name of the kind cluster.")
    66  	kindNodeImage = flag.String("kind-node-image", "", "(kind only) name:tag of the node image to start the cluster. If build is enabled, this is ignored and built image is used.")
    67  )
    68  
    69  var (
    70  	kindBinaryStableHashes = map[string]string{
    71  		"kind-linux-amd64":   "9a64f1774cdf24dad5f92e1299058b371c4e3f09d2f9eb281e91ed0777bd1e13",
    72  		"kind-darwin-amd64":  "b6a8fe2b3b53930a1afa4f91b033cdc24b0f6c628d993abaa9e40b57d261162a",
    73  		"kind-windows-amd64": "df327d1e7f8bb41dfd5b1a69c5bc7a8d4bad95bb933562ca367a3a45b6c6ca04",
    74  	}
    75  )
    76  
    77  // Deployer is an object the satisfies the kubetest main deployer interface.
    78  type Deployer struct {
    79  	control            *process.Control
    80  	buildType          string
    81  	configPath         string
    82  	importPathK8s      string
    83  	importPathKind     string
    84  	kindBinaryDir      string
    85  	kindBinaryVersion  string
    86  	kindBinaryPath     string
    87  	kindKubeconfigPath string
    88  	kindNodeImage      string
    89  	kindBaseImage      string
    90  	kindClusterName    string
    91  }
    92  
    93  // NewDeployer creates a new kind deployer.
    94  func NewDeployer(ctl *process.Control, buildType string) (*Deployer, error) {
    95  	k, err := initializeDeployer(ctl, buildType)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	return k, nil
   100  }
   101  
   102  // initializeDeployer initializers the kind deployer flags.
   103  func initializeDeployer(ctl *process.Control, buildType string) (*Deployer, error) {
   104  	if ctl == nil {
   105  		return nil, fmt.Errorf("kind deployer received nil Control")
   106  	}
   107  	// get the user's HOME
   108  	kindBinaryDir := filepath.Join(os.Getenv("HOME"), kindBinarySubDir)
   109  
   110  	// Ensure the kind binary dir is added in $PATH.
   111  	err := os.MkdirAll(kindBinaryDir, 0770)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	path := os.Getenv("PATH")
   116  	if !strings.Contains(path, kindBinaryDir) {
   117  		if err := os.Setenv("PATH", kindBinaryDir+":"+path); err != nil {
   118  			return nil, err
   119  		}
   120  	}
   121  
   122  	kubeconfigPath := *kindKubeconfigPath
   123  	if kubeconfigPath == "" {
   124  		// Create directory for the cluster kube config
   125  		kindClusterDir := filepath.Join(kindBinaryDir, *kindClusterName)
   126  		if err := os.MkdirAll(kindClusterDir, 0770); err != nil {
   127  			return nil, err
   128  		}
   129  		kubeconfigPath = filepath.Join(kindClusterDir, "kubeconfig")
   130  	}
   131  
   132  	d := &Deployer{
   133  		control:            ctl,
   134  		buildType:          buildType,
   135  		configPath:         *kindConfigPath,
   136  		kindBinaryDir:      kindBinaryDir,
   137  		kindBinaryPath:     filepath.Join(kindBinaryDir, "kind"),
   138  		kindBinaryVersion:  *kindBinaryVersion,
   139  		kindKubeconfigPath: kubeconfigPath,
   140  		kindNodeImage:      *kindNodeImage,
   141  		kindClusterName:    *kindClusterName,
   142  	}
   143  	// Obtain the import paths for k8s and kind
   144  	d.importPathK8s, err = d.getImportPath("k8s.io/kubernetes")
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	d.importPathKind, err = d.getImportPath("sigs.k8s.io/kind")
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	if kindBaseImage != nil {
   154  		d.kindBaseImage = *kindBaseImage
   155  	}
   156  	// ensure we have the kind binary
   157  	if err := d.prepareKindBinary(); err != nil {
   158  		return nil, err
   159  	}
   160  	return d, nil
   161  }
   162  
   163  // getImportPath does a naive concat between GOPATH, "src" and a user provided path.
   164  func (d *Deployer) getImportPath(path string) (string, error) {
   165  	o, err := d.control.Output(exec.Command("go", "env", "GOPATH"))
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	trimmed := strings.TrimSuffix(string(o), "\n")
   170  	log.Printf("kind.go:getImportPath(): %s", trimmed)
   171  	return filepath.Join(trimmed, "src", path), nil
   172  }
   173  
   174  // setKubeConfigEnv sets the KUBECONFIG environment variable.
   175  func (d *Deployer) setKubeConfigEnv() error {
   176  	log.Println("kind.go:setKubeConfigEnv()")
   177  	return os.Setenv("KUBECONFIG", d.kindKubeconfigPath)
   178  }
   179  
   180  // prepareKindBinary either builds kind from source or pulls a binary from GitHub.
   181  func (d *Deployer) prepareKindBinary() error {
   182  	log.Println("kind.go:prepareKindBinary()")
   183  	switch d.kindBinaryVersion {
   184  	case kindBinaryBuild:
   185  		log.Println("Building a kind binary from source.")
   186  		// Build the kind binary.
   187  		cmd := exec.Command("make", "install", "INSTALL_DIR="+d.kindBinaryDir)
   188  		cmd.Dir = d.importPathKind
   189  		if err := d.control.FinishRunning(cmd); err != nil {
   190  			return err
   191  		}
   192  	case kindBinaryStable:
   193  		// ensure a stable kind binary.
   194  		kindPlatformBinary := fmt.Sprintf("kind-%s-%s", runtime.GOOS, runtime.GOARCH)
   195  		if haveStableBinary(d.kindBinaryPath, kindPlatformBinary) {
   196  			log.Printf("Found stable kind binary at %q", d.kindBinaryPath)
   197  			return nil
   198  		}
   199  		// we don't have it, so download it
   200  		binary := fmt.Sprintf("kind-%s-%s", runtime.GOOS, runtime.GOARCH)
   201  		url := fmt.Sprintf("https://github.com/kubernetes-sigs/kind/releases/download/%s/%s", kindBinaryStableTag, binary)
   202  		log.Printf("Downloading a stable kind binary from GitHub: %s, tag: %s", binary, kindBinaryStableTag)
   203  		f, err := os.OpenFile(d.kindBinaryPath, os.O_RDWR|os.O_CREATE, 0770)
   204  		if err != nil {
   205  			return err
   206  		}
   207  		defer f.Close()
   208  		if err := downloadFromURL(url, f); err != nil {
   209  			return err
   210  		}
   211  	default:
   212  		return fmt.Errorf("unknown kind binary version value: %s", d.kindBinaryVersion)
   213  	}
   214  	return nil
   215  }
   216  
   217  // Build handles building kubernetes / kubectl / the node image / ginkgo.
   218  func (d *Deployer) Build() error {
   219  	log.Println("kind.go:Build()")
   220  	// Adapt the build type if needed.
   221  	var buildType string
   222  	var buildNodeImage string
   223  	switch d.buildType {
   224  	case "":
   225  		// The default option is to use a pre-build image.
   226  		log.Println("Skipping the kind node image build.")
   227  		return nil
   228  	case "quick":
   229  		// This is the default build type in kind.
   230  		buildType = "docker"
   231  		buildNodeImage = kindNodeImageLatest
   232  	default:
   233  		// Other types and 'bazel' are handled transparently here.
   234  		buildType = d.buildType
   235  		buildNodeImage = kindNodeImageLatest
   236  	}
   237  
   238  	args := []string{"build", "node-image", "--type=" + buildType, flagLogLevel, "--kube-root=" + d.importPathK8s}
   239  	if buildNodeImage != "" {
   240  		args = append(args, "--image="+buildNodeImage)
   241  		// override user-specified node image
   242  		d.kindNodeImage = buildNodeImage
   243  	}
   244  	if d.kindBaseImage != "" {
   245  		args = append(args, "--base-image="+d.kindBaseImage)
   246  	}
   247  
   248  	// Build the node image (including kubernetes)
   249  	cmd := exec.Command("kind", args...)
   250  	if err := d.control.FinishRunning(cmd); err != nil {
   251  		return err
   252  	}
   253  
   254  	// Ginkgo v1 is used by Kubernetes 1.24 and earlier and exists in the vendor directory.
   255  	// Historically it has been built with the "vendor" prefix.
   256  	ginkgoTarget := "vendor/github.com/onsi/ginkgo/ginkgo"
   257  	if _, err := os.Stat(ginkgoTarget); os.IsNotExist(err) {
   258  		// If the directory doesn't exist, then we must be on Kubernetes >= 1.25 with Ginkgo V2.
   259  		// The "vendor" prefix is no longer needed.
   260  		ginkgoTarget = "github.com/onsi/ginkgo/v2/ginkgo"
   261  	}
   262  
   263  	// Build binaries for the host, including kubectl, ginkgo, e2e.test
   264  	if d.buildType != "bazel" {
   265  		cmd := exec.Command(
   266  			"make", "all",
   267  			"WHAT=cmd/kubectl test/e2e/e2e.test"+" "+ginkgoTarget,
   268  		)
   269  		cmd.Dir = d.importPathK8s
   270  		if err := d.control.FinishRunning(cmd); err != nil {
   271  			return err
   272  		}
   273  		// Copy kubectl to the kind binary path.
   274  		cmd = exec.Command("cp", "-f", "./_output/local/go/bin/kubectl", d.kindBinaryDir)
   275  		cmd.Dir = d.importPathK8s
   276  		if err := d.control.FinishRunning(cmd); err != nil {
   277  			return err
   278  		}
   279  	} else {
   280  		// make build
   281  		cmd := exec.Command(
   282  			"bazel", "build",
   283  			"//cmd/kubectl", "//test/e2e:e2e.test",
   284  			"//"+ginkgoTarget,
   285  		)
   286  		cmd.Dir = d.importPathK8s
   287  		if err := d.control.FinishRunning(cmd); err != nil {
   288  			return err
   289  		}
   290  		// Copy kubectl to the kind binary path.
   291  		kubectlPath := fmt.Sprintf(
   292  			"./bazel-bin/cmd/kubectl/%s_%s_pure_stripped/kubectl",
   293  			runtime.GOOS, runtime.GOARCH,
   294  		)
   295  		cmd = exec.Command("cp", "-f", kubectlPath, d.kindBinaryDir)
   296  		cmd.Dir = d.importPathK8s
   297  		if err := d.control.FinishRunning(cmd); err != nil {
   298  			return err
   299  		}
   300  	}
   301  
   302  	return nil
   303  }
   304  
   305  // Up creates a kind cluster. Allows passing node image and config.
   306  func (d *Deployer) Up() error {
   307  	log.Println("kind.go:Up()")
   308  	args := []string{"create", "cluster", "--retain", "--wait=1m", flagLogLevel}
   309  
   310  	// Handle the config flag.
   311  	if d.configPath != "" {
   312  		args = append(args, "--config="+d.configPath)
   313  	}
   314  
   315  	// Handle the node image flag if we built a new node image.
   316  	if d.kindNodeImage != "" {
   317  		args = append(args, "--image="+d.kindNodeImage)
   318  	}
   319  
   320  	// Use a specific cluster name.
   321  	if d.kindClusterName != "" {
   322  		args = append(args, "--name="+d.kindClusterName)
   323  	}
   324  
   325  	// Use specific path for the kubeconfig
   326  	if d.kindKubeconfigPath != "" {
   327  		args = append(args, "--kubeconfig="+d.kindKubeconfigPath)
   328  	}
   329  
   330  	// Build the kind cluster.
   331  	cmd := exec.Command("kind", args...)
   332  	if err := d.control.FinishRunning(cmd); err != nil {
   333  		return err
   334  	}
   335  	log.Println("*************************************************************************************************")
   336  	log.Println("Cluster is UP")
   337  	log.Printf("Run: \"export KUBECONFIG=%s\" to access to it\n", d.kindKubeconfigPath)
   338  	log.Println("*************************************************************************************************")
   339  	return nil
   340  }
   341  
   342  // IsUp verifies if the cluster created by Up() is functional.
   343  func (d *Deployer) IsUp() error {
   344  	log.Println("kind.go:IsUp()")
   345  
   346  	// Check if kubectl reports nodes.
   347  	cmd, err := d.KubectlCommand()
   348  	if err != nil {
   349  		return err
   350  	}
   351  	cmd.Args = append(cmd.Args, []string{"get", "nodes", "--no-headers"}...)
   352  	o, err := d.control.Output(cmd)
   353  	if err != nil {
   354  		return err
   355  	}
   356  	trimmed := strings.TrimSpace(string(o))
   357  	n := 0
   358  	if trimmed != "" {
   359  		n = len(strings.Split(trimmed, "\n"))
   360  	}
   361  	if n <= 0 {
   362  		return fmt.Errorf("cluster found, but %d nodes reported", n)
   363  	}
   364  	return nil
   365  }
   366  
   367  // DumpClusterLogs dumps the logs for this cluster in localPath.
   368  func (d *Deployer) DumpClusterLogs(localPath, gcsPath string) error {
   369  	log.Println("kind.go:DumpClusterLogs()")
   370  	args := []string{"export", "logs", localPath, flagLogLevel}
   371  
   372  	// Use a specific cluster name.
   373  	if d.kindClusterName != "" {
   374  		args = append(args, "--name="+d.kindClusterName)
   375  	}
   376  
   377  	cmd := exec.Command("kind", args...)
   378  	if err := d.control.FinishRunning(cmd); err != nil {
   379  		log.Printf("kind.go:DumpClusterLogs(): ignoring error: %v", err)
   380  	}
   381  	return nil
   382  }
   383  
   384  // TestSetup is a NO-OP in this deployer.
   385  func (d *Deployer) TestSetup() error {
   386  	log.Println("kind.go:TestSetup()")
   387  
   388  	// set conformance env so ginkgo.sh etc won't try to do provider setup
   389  	if err := os.Setenv("KUBERNETES_CONFORMANCE_TEST", "y"); err != nil {
   390  		return err
   391  	}
   392  
   393  	// Proceed only if a cluster exists.
   394  	exists, err := d.clusterExists()
   395  	if err != nil {
   396  		return err
   397  	}
   398  	if !exists {
   399  		log.Printf("kind.go:TestSetup(): no such cluster %q; skipping the setup of KUBECONFIG!", d.kindClusterName)
   400  		return nil
   401  	}
   402  
   403  	// set KUBECONFIG
   404  	if err = d.setKubeConfigEnv(); err != nil {
   405  		return err
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  // clusterExists checks if a kind cluster with 'name' exists
   412  func (d *Deployer) clusterExists() (bool, error) {
   413  	log.Println("kind.go:clusterExists()")
   414  
   415  	cmd := exec.Command("kind")
   416  	cmd.Args = append(cmd.Args, []string{"get", "clusters"}...)
   417  	out, err := d.control.Output(cmd)
   418  	if err != nil {
   419  		return false, err
   420  	}
   421  
   422  	lines := strings.Split(string(out), "\n")
   423  	for _, line := range lines {
   424  		if line == d.kindClusterName {
   425  			log.Printf("kind.go:clusterExists(): found %q", d.kindClusterName)
   426  			return true, nil
   427  		}
   428  	}
   429  	return false, nil
   430  }
   431  
   432  // Down tears down the cluster.
   433  func (d *Deployer) Down() error {
   434  	log.Println("kind.go:Down()")
   435  
   436  	// Proceed only if a cluster exists.
   437  	exists, err := d.clusterExists()
   438  	if err != nil {
   439  		return err
   440  	}
   441  	if !exists {
   442  		log.Printf("kind.go:Down(): no such cluster %q; skipping 'delete'!", d.kindClusterName)
   443  		return nil
   444  	}
   445  
   446  	log.Printf("kind.go:Down(): deleting cluster: %s", d.kindClusterName)
   447  	args := []string{"delete", "cluster", flagLogLevel}
   448  
   449  	// Use a specific cluster name.
   450  	if d.kindClusterName != "" {
   451  		args = append(args, "--name="+d.kindClusterName)
   452  	}
   453  
   454  	// Delete the cluster.
   455  	cmd := exec.Command("kind", args...)
   456  	if err := d.control.FinishRunning(cmd); err != nil {
   457  		return err
   458  	}
   459  
   460  	if d.kindClusterName != "" {
   461  		kindClusterDir := filepath.Join(d.kindBinaryDir, d.kindClusterName)
   462  		if _, err := os.Stat(kindClusterDir); !os.IsNotExist(err) {
   463  			if err := os.RemoveAll(kindClusterDir); err != nil {
   464  				return err
   465  			}
   466  		}
   467  	}
   468  	return nil
   469  }
   470  
   471  // GetClusterCreated is unimplemented.GetClusterCreated
   472  func (d *Deployer) GetClusterCreated(gcpProject string) (time.Time, error) {
   473  	log.Println("kind.go:GetClusterCreated()")
   474  	return time.Time{}, errors.New("not implemented")
   475  }
   476  
   477  // KubectlCommand returns the exec.Cmd command for kubectl.
   478  func (d *Deployer) KubectlCommand() (*exec.Cmd, error) {
   479  	log.Println("kind.go:KubectlCommand()")
   480  	if err := d.setKubeConfigEnv(); err != nil {
   481  		return nil, err
   482  	}
   483  	// Avoid using ./cluster/kubectl.sh
   484  	// TODO(bentheelder): cache this
   485  	return exec.Command("kubectl"), nil
   486  }
   487  
   488  // downloadFromURL downloads from a url to f
   489  func downloadFromURL(url string, f *os.File) error {
   490  	log.Printf("kind.go:downloadFromURL(): %s", url)
   491  	// TODO(bentheelder): is this long enough?
   492  	timeout := time.Duration(60 * time.Second)
   493  	client := http.Client{
   494  		Timeout: timeout,
   495  	}
   496  	resp, err := client.Get(url)
   497  	if err != nil {
   498  		return err
   499  	}
   500  	defer resp.Body.Close()
   501  	defer f.Sync()
   502  	_, err = io.Copy(f, resp.Body)
   503  	return err
   504  }
   505  
   506  // returns true if the binary at expected path exists and
   507  // matches the expected hash of kindPlatformBinary
   508  func haveStableBinary(expectedPath, kindPlatformBinary string) bool {
   509  	if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
   510  		log.Printf("kind binary not present at %s", expectedPath)
   511  		return false
   512  	}
   513  	expectedHash, ok := kindBinaryStableHashes[kindPlatformBinary]
   514  	if !ok {
   515  		return false
   516  	}
   517  	hash, err := hashFile(expectedPath)
   518  	if err != nil {
   519  		return false
   520  	}
   521  	hashMatches := expectedHash == hash
   522  	if !hashMatches {
   523  		log.Printf("kind binary present with hash %q at %q, but expected hash %q", hash, expectedPath, expectedHash)
   524  	}
   525  	return hashMatches
   526  }
   527  
   528  // computes the sha256sum of the file at path
   529  func hashFile(path string) (string, error) {
   530  	f, err := os.Open(path)
   531  	if err != nil {
   532  		return "", err
   533  	}
   534  	defer f.Close()
   535  	h := sha256.New()
   536  	if _, err := io.Copy(h, f); err != nil {
   537  		return "", err
   538  	}
   539  	return hex.EncodeToString(h.Sum(nil)), nil
   540  }