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 }