dev-tools/mage/kubernetes/kubernetes.go (146 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package kubernetes
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/magefile/mage/mg"
"github.com/elastic/elastic-agent/dev-tools/mage"
)
func init() {
mage.RegisterIntegrationTester(&IntegrationTester{})
}
// IntegrationTester integration tester
type IntegrationTester struct {
}
// Name returns kubernetes name.
func (d *IntegrationTester) Name() string {
return "kubernetes"
}
// Use determines if this tester should be used.
func (d *IntegrationTester) Use(dir string) (bool, error) {
kubernetesFile := filepath.Join(dir, "kubernetes.yml")
if _, err := os.Stat(kubernetesFile); !os.IsNotExist(err) {
return true, nil
}
return false, nil
}
// HasRequirements ensures that the required kubectl are installed.
func (d *IntegrationTester) HasRequirements() error {
if err := mage.HaveKubectl(); err != nil {
return err
}
return nil
}
// StepRequirements returns the steps required for this tester.
func (d *IntegrationTester) StepRequirements() mage.IntegrationTestSteps {
return mage.IntegrationTestSteps{&mage.IntegrationTestStep{}, &KindIntegrationTestStep{}}
}
// Test performs the tests with kubernetes.
func (d *IntegrationTester) Test(dir string, mageTarget string, env map[string]string) error {
stdOut := io.Discard
stdErr := io.Discard
if mg.Verbose() {
stdOut = os.Stdout
stdErr = os.Stderr
}
manifestPath := filepath.Join(dir, "kubernetes.yml")
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
// defensive, as `Use` should cause this runner not to be used if no file.
return fmt.Errorf("no kubernetes.yml")
}
kubeConfig := env["KUBECONFIG"]
if kubeConfig == "" {
kubeConfig = env["KUBE_CONFIG"]
}
if kubeConfig == "" {
fmt.Println("Skip running tests inside of kubernetes no KUBECONFIG defined.")
return nil
}
if mg.Verbose() {
fmt.Println(">> Applying module manifest to cluster...")
}
// Determine the path to use inside the pod.
repo, err := mage.GetProjectRepoInfo()
if err != nil {
return err
}
magePath := filepath.Join("/go/src", repo.CanonicalRootImportPath, repo.SubDir, "build/mage-linux-amd64")
// Apply the manifest from the dir. This is the requirements for the tests that will
// run inside the cluster.
if err := KubectlApply(env, stdOut, stdErr, manifestPath); err != nil {
return fmt.Errorf("failed to apply manifest %s: %w", manifestPath, err)
}
defer func() {
if mg.Verbose() {
fmt.Println(">> Deleting module manifest from cluster...")
}
if err := KubectlDelete(env, stdOut, stdErr, manifestPath); err != nil {
log.Printf("%s", fmt.Errorf("failed to apply manifest %s: %w", manifestPath, err))
}
}()
err = waitKubeStateMetricsReadiness(env, stdOut, stdErr)
if err != nil {
return err
}
// Pass all environment variables inside the pod, except for KUBECONFIG as the test
// should use the environment set by kubernetes on the pod.
insideEnv := map[string]string{}
for envKey, envVal := range env {
if envKey != "KUBECONFIG" && envKey != "KUBE_CONFIG" {
insideEnv[envKey] = envVal
}
}
destDir := filepath.Join("/go/src", repo.CanonicalRootImportPath)
workDir := filepath.Join(destDir, repo.SubDir)
remote, err := NewKubeRemote(kubeConfig, "default", kubernetesClusterName(), workDir, destDir, repo.RootDir)
if err != nil {
return err
}
// Uses `os.Stdout` directly as its output should always be shown.
err = remote.Run(insideEnv, os.Stdout, stdErr, magePath, mageTarget)
if err != nil {
return err
}
return nil
}
// InsideTest performs the tests inside of environment.
func (d *IntegrationTester) InsideTest(test func() error) error {
return test()
}
// waitKubeStateMetricsReadiness waits until kube-state-metrics Pod is ready to receive requests
func waitKubeStateMetricsReadiness(env map[string]string, stdOut, stdErr io.Writer) error {
checkKubeStateMetricsReadyAttempts := 10
readyAttempts := 1
for {
err := KubectlWait(env, stdOut, stdErr, "condition=ready", "pod", "app=kube-state-metrics")
if err != nil {
if mg.Verbose() {
fmt.Println("Kube-state-metrics is not ready yet...retrying")
}
} else {
break
}
if readyAttempts > checkKubeStateMetricsReadyAttempts {
return fmt.Errorf("timeout waiting for kube-state-metrics: %w", err)
}
time.Sleep(6 * time.Second)
readyAttempts++
}
// kube-state-metrics ready, return with no error
return nil
}
// kubernetesClusterName generates a name for the Kubernetes cluster.
func kubernetesClusterName() string {
commit, err := mage.CommitHash()
if err != nil {
panic(fmt.Errorf("failed to construct kind cluster name: %w", err))
}
version, err := mage.BeatQualifiedVersion()
if err != nil {
panic(fmt.Errorf("failed to construct kind cluster name: %w", err))
}
version = strings.NewReplacer(".", "-").Replace(version)
clusterName := "{{.BeatName}}-{{.Version}}-{{.ShortCommit}}-{{.StackEnvironment}}"
clusterName = mage.MustExpand(clusterName, map[string]interface{}{
"StackEnvironment": mage.StackEnvironment,
"ShortCommit": commit[:10],
"Version": version,
})
// The cluster name may be used as a component of Kubernetes resource names.
// kind does this, for example.
//
// Since Kubernetes resources are required to have names that are valid DNS
// names, we should ensure that the cluster name also meets this criterion.
subDomainPattern := `^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$`
// Note that underscores, in particular, are not permitted.
matched, err := regexp.MatchString(subDomainPattern, clusterName)
if err != nil {
panic(fmt.Errorf("error while validating kind cluster name: %w", err))
}
if !matched {
panic("constructed invalid kind cluster name")
}
return clusterName
}