sigs.k8s.io/kubebuilder/v3@v3.14.0/test/e2e/utils/test_context.go (about)

     1  /*
     2  Copyright 2019 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 utils
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"os/exec"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	log "github.com/sirupsen/logrus"
    28  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
    29  
    30  	. "github.com/onsi/ginkgo/v2" //nolint:golint,revive
    31  )
    32  
    33  const (
    34  	certmanagerVersion        = "v1.5.3"
    35  	certmanagerURLTmpl        = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
    36  	prometheusOperatorVersion = "0.51"
    37  	prometheusOperatorURL     = "https://raw.githubusercontent.com/prometheus-operator/" +
    38  		"prometheus-operator/release-%s/bundle.yaml"
    39  )
    40  
    41  // TestContext specified to run e2e tests
    42  type TestContext struct {
    43  	*CmdContext
    44  	TestSuffix   string
    45  	Domain       string
    46  	Group        string
    47  	Version      string
    48  	Kind         string
    49  	Resources    string
    50  	ImageName    string
    51  	BinaryName   string
    52  	Kubectl      *Kubectl
    53  	K8sVersion   *KubernetesVersion
    54  	IsRestricted bool
    55  }
    56  
    57  // NewTestContext init with a random suffix for test TestContext stuff,
    58  // to avoid conflict when running tests synchronously.
    59  func NewTestContext(binaryName string, env ...string) (*TestContext, error) {
    60  	testSuffix, err := util.RandomSuffix()
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	cc := &CmdContext{
    66  		Env: env,
    67  	}
    68  
    69  	// Use kubectl to get Kubernetes client and cluster version.
    70  	kubectl := &Kubectl{
    71  		Namespace:      fmt.Sprintf("e2e-%s-system", testSuffix),
    72  		ServiceAccount: fmt.Sprintf("e2e-%s-controller-manager", testSuffix),
    73  		CmdContext:     cc,
    74  	}
    75  	k8sVersion, err := kubectl.Version()
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	// Set CmdContext.Dir after running Kubectl.Version() because dir does not exist yet.
    81  	if cc.Dir, err = filepath.Abs("e2e-" + testSuffix); err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	return &TestContext{
    86  		TestSuffix: testSuffix,
    87  		Domain:     "example.com" + testSuffix,
    88  		Group:      "bar" + testSuffix,
    89  		Version:    "v1alpha1",
    90  		Kind:       "Foo" + testSuffix,
    91  		Resources:  "foo" + testSuffix + "s",
    92  		ImageName:  "e2e-test/controller-manager:" + testSuffix,
    93  		CmdContext: cc,
    94  		Kubectl:    kubectl,
    95  		K8sVersion: &k8sVersion,
    96  		BinaryName: binaryName,
    97  	}, nil
    98  }
    99  
   100  func warnError(err error) {
   101  	fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
   102  }
   103  
   104  // Prepare prepares the test environment.
   105  func (t *TestContext) Prepare() error {
   106  	// Remove tools used by projects in the environment so the correct version is downloaded for each test.
   107  	fmt.Fprintln(GinkgoWriter, "cleaning up tools")
   108  	for _, toolName := range []string{"controller-gen", "kustomize"} {
   109  		if toolPath, err := exec.LookPath(toolName); err == nil {
   110  			if err := os.RemoveAll(toolPath); err != nil {
   111  				return err
   112  			}
   113  		}
   114  	}
   115  
   116  	fmt.Fprintf(GinkgoWriter, "preparing testing directory: %s\n", t.Dir)
   117  	return os.MkdirAll(t.Dir, 0o755)
   118  }
   119  
   120  // makeCertManagerURL returns a kubectl-able URL for the cert-manager bundle.
   121  func (t *TestContext) makeCertManagerURL() string {
   122  	return fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
   123  }
   124  
   125  func (t *TestContext) makePrometheusOperatorURL() string {
   126  	return fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
   127  }
   128  
   129  // InstallCertManager installs the cert manager bundle. If hasv1beta1CRs is true,
   130  // the legacy version (which uses v1alpha2 CRs) is installed.
   131  func (t *TestContext) InstallCertManager() error {
   132  	url := t.makeCertManagerURL()
   133  	if _, err := t.Kubectl.Apply(false, "-f", url, "--validate=false"); err != nil {
   134  		return err
   135  	}
   136  	// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
   137  	// was re-installed after uninstalling on a cluster.
   138  	_, err := t.Kubectl.Wait(false, "deployment.apps/cert-manager-webhook",
   139  		"--for", "condition=Available",
   140  		"--namespace", "cert-manager",
   141  		"--timeout", "5m",
   142  	)
   143  	return err
   144  }
   145  
   146  // UninstallCertManager uninstalls the cert manager bundle.
   147  func (t *TestContext) UninstallCertManager() {
   148  	url := t.makeCertManagerURL()
   149  	if _, err := t.Kubectl.Delete(false, "-f", url); err != nil {
   150  		warnError(err)
   151  	}
   152  }
   153  
   154  // InstallPrometheusOperManager installs the prometheus manager bundle.
   155  func (t *TestContext) InstallPrometheusOperManager() error {
   156  	url := t.makePrometheusOperatorURL()
   157  	_, err := t.Kubectl.Apply(false, "-f", url)
   158  	return err
   159  }
   160  
   161  // UninstallPrometheusOperManager uninstalls the prometheus manager bundle.
   162  func (t *TestContext) UninstallPrometheusOperManager() {
   163  	url := t.makePrometheusOperatorURL()
   164  	if _, err := t.Kubectl.Delete(false, "-f", url); err != nil {
   165  		warnError(err)
   166  	}
   167  }
   168  
   169  // CleanupManifests is a helper func to run kustomize build and pipe the output to kubectl delete -f -
   170  func (t *TestContext) CleanupManifests(dir string) {
   171  	kustomizePath := filepath.Join(t.Dir, "bin", "kustomize")
   172  	if _, err := os.Stat(kustomizePath); err != nil {
   173  		// Just fail below with an error about kustomize not being installed globally.
   174  		kustomizePath = "kustomize"
   175  	}
   176  	cmd := exec.Command(kustomizePath, "build", dir)
   177  	output, err := t.Run(cmd)
   178  	if err != nil {
   179  		warnError(err)
   180  	}
   181  	if _, err := t.Kubectl.WithInput(string(output)).Command("delete", "-f", "-"); err != nil {
   182  		warnError(err)
   183  	}
   184  }
   185  
   186  // Init is for running `kubebuilder init`
   187  func (t *TestContext) Init(initOptions ...string) error {
   188  	initOptions = append([]string{"init"}, initOptions...)
   189  	//nolint:gosec
   190  	cmd := exec.Command(t.BinaryName, initOptions...)
   191  	_, err := t.Run(cmd)
   192  	return err
   193  }
   194  
   195  // Edit is for running `kubebuilder edit`
   196  func (t *TestContext) Edit(editOptions ...string) error {
   197  	editOptions = append([]string{"edit"}, editOptions...)
   198  	//nolint:gosec
   199  	cmd := exec.Command(t.BinaryName, editOptions...)
   200  	_, err := t.Run(cmd)
   201  	return err
   202  }
   203  
   204  // CreateAPI is for running `kubebuilder create api`
   205  func (t *TestContext) CreateAPI(resourceOptions ...string) error {
   206  	resourceOptions = append([]string{"create", "api"}, resourceOptions...)
   207  	//nolint:gosec
   208  	cmd := exec.Command(t.BinaryName, resourceOptions...)
   209  	_, err := t.Run(cmd)
   210  	return err
   211  }
   212  
   213  // CreateWebhook is for running `kubebuilder create webhook`
   214  func (t *TestContext) CreateWebhook(resourceOptions ...string) error {
   215  	resourceOptions = append([]string{"create", "webhook"}, resourceOptions...)
   216  	//nolint:gosec
   217  	cmd := exec.Command(t.BinaryName, resourceOptions...)
   218  	_, err := t.Run(cmd)
   219  	return err
   220  }
   221  
   222  // Regenerate is for running `kubebuilder alpha generate`
   223  func (t *TestContext) Regenerate(resourceOptions ...string) error {
   224  	resourceOptions = append([]string{"alpha", "generate"}, resourceOptions...)
   225  	//nolint:gosec
   226  	cmd := exec.Command(t.BinaryName, resourceOptions...)
   227  	_, err := t.Run(cmd)
   228  	return err
   229  }
   230  
   231  // Make is for running `make` with various targets
   232  func (t *TestContext) Make(makeOptions ...string) error {
   233  	cmd := exec.Command("make", makeOptions...)
   234  	_, err := t.Run(cmd)
   235  	return err
   236  }
   237  
   238  // Tidy runs `go mod tidy` so that go 1.16 build doesn't fail.
   239  // See https://blog.golang.org/go116-module-changes#TOC_3.
   240  func (t *TestContext) Tidy() error {
   241  	cmd := exec.Command("go", "mod", "tidy")
   242  	_, err := t.Run(cmd)
   243  	return err
   244  }
   245  
   246  // Destroy is for cleaning up the docker images for testing
   247  func (t *TestContext) Destroy() {
   248  	//nolint:gosec
   249  	// if image name is not present or not provided skip execution of docker command
   250  	if t.ImageName != "" {
   251  		// Check white space from image name
   252  		if len(strings.TrimSpace(t.ImageName)) == 0 {
   253  			log.Println("Image not set, skip cleaning up of docker image")
   254  		} else {
   255  			cmd := exec.Command("docker", "rmi", "-f", t.ImageName)
   256  			if _, err := t.Run(cmd); err != nil {
   257  				warnError(err)
   258  			}
   259  		}
   260  
   261  	}
   262  	if err := os.RemoveAll(t.Dir); err != nil {
   263  		warnError(err)
   264  	}
   265  }
   266  
   267  // CreateManagerNamespace will create the namespace where the manager is deployed
   268  func (t *TestContext) CreateManagerNamespace() error {
   269  	_, err := t.Kubectl.Command("create", "ns", t.Kubectl.Namespace)
   270  	return err
   271  }
   272  
   273  // LabelAllNamespacesToWarnAboutRestricted will label all namespaces so that we can verify
   274  // if a warning with `Warning: would violate PodSecurity` will be raised when the manifests are applied
   275  func (t *TestContext) LabelAllNamespacesToWarnAboutRestricted() error {
   276  	_, err := t.Kubectl.Command("label", "--overwrite", "ns", "--all",
   277  		"pod-security.kubernetes.io/audit=restricted",
   278  		"pod-security.kubernetes.io/enforce-version=v1.24",
   279  		"pod-security.kubernetes.io/warn=restricted")
   280  	return err
   281  }
   282  
   283  // LoadImageToKindCluster loads a local docker image to the kind cluster
   284  func (t *TestContext) LoadImageToKindCluster() error {
   285  	cluster := "kind"
   286  	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
   287  		cluster = v
   288  	}
   289  	kindOptions := []string{"load", "docker-image", t.ImageName, "--name", cluster}
   290  	cmd := exec.Command("kind", kindOptions...)
   291  	_, err := t.Run(cmd)
   292  	return err
   293  }
   294  
   295  // LoadImageToKindClusterWithName loads a local docker image with the name informed to the kind cluster
   296  func (t TestContext) LoadImageToKindClusterWithName(image string) error {
   297  	cluster := "kind"
   298  	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
   299  		cluster = v
   300  	}
   301  	kindOptions := []string{"load", "docker-image", "--name", cluster, image}
   302  	cmd := exec.Command("kind", kindOptions...)
   303  	_, err := t.Run(cmd)
   304  	return err
   305  }
   306  
   307  // CmdContext provides context for command execution
   308  type CmdContext struct {
   309  	// environment variables in k=v format.
   310  	Env   []string
   311  	Dir   string
   312  	Stdin io.Reader
   313  }
   314  
   315  // Run executes the provided command within this context
   316  func (cc *CmdContext) Run(cmd *exec.Cmd) ([]byte, error) {
   317  	cmd.Dir = cc.Dir
   318  	cmd.Env = append(os.Environ(), cc.Env...)
   319  	cmd.Stdin = cc.Stdin
   320  	command := strings.Join(cmd.Args, " ")
   321  	fmt.Fprintf(GinkgoWriter, "running: %s\n", command)
   322  	output, err := cmd.CombinedOutput()
   323  	if err != nil {
   324  		return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output))
   325  	}
   326  
   327  	return output, nil
   328  }
   329  
   330  // AllowProjectBeMultiGroup will update the PROJECT file with the information to allow we scaffold
   331  // apis with different groups. be available.
   332  func (t *TestContext) AllowProjectBeMultiGroup() error {
   333  	const multiGroup = `multigroup: true
   334  `
   335  	projectBytes, err := os.ReadFile(filepath.Join(t.Dir, "PROJECT"))
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	projectBytes = append([]byte(multiGroup), projectBytes...)
   341  	err = os.WriteFile(filepath.Join(t.Dir, "PROJECT"), projectBytes, 0o644)
   342  	if err != nil {
   343  		return err
   344  	}
   345  	return nil
   346  }