sigs.k8s.io/kubebuilder/v3@v3.14.0/test/e2e/v4/plugin_cluster_test.go (about)

     1  /*
     2  Copyright 2020 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 v4
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"os"
    23  	"os/exec"
    24  	"path/filepath"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
    30  
    31  	//nolint:golint
    32  	//nolint:revive
    33  	. "github.com/onsi/ginkgo/v2"
    34  
    35  	//nolint:golint
    36  	//nolint:revive
    37  	. "github.com/onsi/gomega"
    38  
    39  	"sigs.k8s.io/kubebuilder/v3/test/e2e/utils"
    40  )
    41  
    42  const (
    43  	tokenRequestRawString = `{"apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest"}`
    44  )
    45  
    46  // tokenRequest is a trimmed down version of the authentication.k8s.io/v1/TokenRequest Type
    47  // that we want to use for extracting the token.
    48  type tokenRequest struct {
    49  	Status struct {
    50  		Token string `json:"token"`
    51  	} `json:"status"`
    52  }
    53  
    54  var _ = Describe("kubebuilder", func() {
    55  	Context("plugin go/v4", func() {
    56  		var kbc *utils.TestContext
    57  
    58  		BeforeEach(func() {
    59  			var err error
    60  			kbc, err = utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on")
    61  			Expect(err).NotTo(HaveOccurred())
    62  			Expect(kbc.Prepare()).To(Succeed())
    63  
    64  			By("installing the cert-manager bundle")
    65  			Expect(kbc.InstallCertManager()).To(Succeed())
    66  
    67  			By("installing the Prometheus operator")
    68  			Expect(kbc.InstallPrometheusOperManager()).To(Succeed())
    69  		})
    70  
    71  		AfterEach(func() {
    72  			By("clean up API objects created during the test")
    73  			kbc.CleanupManifests(filepath.Join("config", "default"))
    74  
    75  			By("uninstalling the Prometheus manager bundle")
    76  			kbc.UninstallPrometheusOperManager()
    77  
    78  			By("uninstalling the cert-manager bundle")
    79  			kbc.UninstallCertManager()
    80  
    81  			By("removing controller image and working dir")
    82  			kbc.Destroy()
    83  		})
    84  		It("should generate a runnable project"+
    85  			" with restricted pods", func() {
    86  			kbc.IsRestricted = true
    87  			GenerateV4(kbc)
    88  			Run(kbc, true, false)
    89  		})
    90  		It("should generate a runnable project without webhooks"+
    91  			" with restricted pods", func() {
    92  			kbc.IsRestricted = true
    93  			GenerateV4WithoutWebhooks(kbc)
    94  			Run(kbc, false, false)
    95  		})
    96  		It("should generate a runnable project"+
    97  			" with the Installer", func() {
    98  			GenerateV4(kbc)
    99  			Run(kbc, false, true)
   100  		})
   101  	})
   102  })
   103  
   104  // Run runs a set of e2e tests for a scaffolded project defined by a TestContext.
   105  func Run(kbc *utils.TestContext, hasWebhook, isToUseInstaller bool) {
   106  	var controllerPodName string
   107  	var err error
   108  
   109  	By("creating manager namespace")
   110  	err = kbc.CreateManagerNamespace()
   111  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   112  
   113  	By("labeling all namespaces to warn about restricted")
   114  	err = kbc.LabelAllNamespacesToWarnAboutRestricted()
   115  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   116  
   117  	By("updating the go.mod")
   118  	err = kbc.Tidy()
   119  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   120  
   121  	By("run make manifests")
   122  	err = kbc.Make("manifests")
   123  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   124  
   125  	By("run make generate")
   126  	err = kbc.Make("generate")
   127  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   128  
   129  	By("building the controller image")
   130  	err = kbc.Make("docker-build", "IMG="+kbc.ImageName)
   131  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   132  
   133  	By("loading the controller docker image into the kind cluster")
   134  	err = kbc.LoadImageToKindCluster()
   135  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   136  
   137  	var output []byte
   138  	if !isToUseInstaller {
   139  		// NOTE: If you want to run the test against a GKE cluster, you will need to grant yourself permission.
   140  		// Otherwise, you may see "... is forbidden: attempt to grant extra privileges"
   141  		// $ kubectl create clusterrolebinding myname-cluster-admin-binding \
   142  		// --clusterrole=cluster-admin --user=myname@mycompany.com
   143  		// https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control
   144  		By("deploying the controller-manager")
   145  
   146  		cmd := exec.Command("make", "deploy", "IMG="+kbc.ImageName)
   147  		output, err = kbc.Run(cmd)
   148  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   149  	} else {
   150  		By("building the installer")
   151  		err = kbc.Make("build-installer", "IMG="+kbc.ImageName)
   152  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   153  
   154  		// NOTE: If you want to run the test against a GKE cluster, you will need to grant yourself permission.
   155  		// Otherwise, you may see "... is forbidden: attempt to grant extra privileges"
   156  		// $ kubectl create clusterrolebinding myname-cluster-admin-binding \
   157  		// --clusterrole=cluster-admin --user=myname@mycompany.com
   158  		// https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control
   159  		By("deploying the controller-manager with the installer")
   160  
   161  		_, err = kbc.Kubectl.Apply(true, "-f", "dist/install.yaml")
   162  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   163  	}
   164  
   165  	if kbc.IsRestricted && !isToUseInstaller {
   166  		By("validating that manager Pod/container(s) are restricted")
   167  		ExpectWithOffset(1, output).NotTo(ContainSubstring("Warning: would violate PodSecurity"))
   168  	}
   169  
   170  	By("validating that the controller-manager pod is running as expected")
   171  	verifyControllerUp := func() error {
   172  		// Get pod name
   173  		podOutput, err := kbc.Kubectl.Get(
   174  			true,
   175  			"pods", "-l", "control-plane=controller-manager",
   176  			"-o", "go-template={{ range .items }}{{ if not .metadata.deletionTimestamp }}{{ .metadata.name }}"+
   177  				"{{ \"\\n\" }}{{ end }}{{ end }}")
   178  		ExpectWithOffset(2, err).NotTo(HaveOccurred())
   179  		podNames := util.GetNonEmptyLines(podOutput)
   180  		if len(podNames) != 1 {
   181  			return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames))
   182  		}
   183  		controllerPodName = podNames[0]
   184  		ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager"))
   185  
   186  		// Validate pod status
   187  		status, err := kbc.Kubectl.Get(
   188  			true,
   189  			"pods", controllerPodName, "-o", "jsonpath={.status.phase}")
   190  		ExpectWithOffset(2, err).NotTo(HaveOccurred())
   191  		if status != "Running" {
   192  			return fmt.Errorf("controller pod in %s status", status)
   193  		}
   194  		return nil
   195  	}
   196  	defer func() {
   197  		out, err := kbc.Kubectl.CommandInNamespace("describe", "all")
   198  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   199  		fmt.Fprintln(GinkgoWriter, out)
   200  	}()
   201  	EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed())
   202  
   203  	By("granting permissions to access the metrics")
   204  	_, err = kbc.Kubectl.Command(
   205  		"create", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
   206  		fmt.Sprintf("--clusterrole=e2e-%s-metrics-reader", kbc.TestSuffix),
   207  		fmt.Sprintf("--serviceaccount=%s:%s", kbc.Kubectl.Namespace, kbc.Kubectl.ServiceAccount))
   208  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   209  
   210  	_ = curlMetrics(kbc)
   211  
   212  	if hasWebhook {
   213  		By("validating that cert-manager has provisioned the certificate Secret")
   214  		EventuallyWithOffset(1, func() error {
   215  			_, err := kbc.Kubectl.Get(
   216  				true,
   217  				"secrets", "webhook-server-cert")
   218  			return err
   219  		}, time.Minute, time.Second).Should(Succeed())
   220  	}
   221  
   222  	By("validating that the Prometheus manager has provisioned the Service")
   223  	EventuallyWithOffset(1, func() error {
   224  		_, err := kbc.Kubectl.Get(
   225  			false,
   226  			"Service", "prometheus-operator")
   227  		return err
   228  	}, time.Minute, time.Second).Should(Succeed())
   229  
   230  	By("validating that the ServiceMonitor for Prometheus is applied in the namespace")
   231  	_, err = kbc.Kubectl.Get(
   232  		true,
   233  		"ServiceMonitor")
   234  	ExpectWithOffset(1, err).NotTo(HaveOccurred())
   235  
   236  	if hasWebhook {
   237  		By("validating that the mutating|validating webhooks have the CA injected")
   238  		verifyCAInjection := func() error {
   239  			mwhOutput, err := kbc.Kubectl.Get(
   240  				false,
   241  				"mutatingwebhookconfigurations.admissionregistration.k8s.io",
   242  				fmt.Sprintf("e2e-%s-mutating-webhook-configuration", kbc.TestSuffix),
   243  				"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
   244  			ExpectWithOffset(2, err).NotTo(HaveOccurred())
   245  			// check that ca should be long enough, because there may be a place holder "\n"
   246  			ExpectWithOffset(2, len(mwhOutput)).To(BeNumerically(">", 10))
   247  
   248  			vwhOutput, err := kbc.Kubectl.Get(
   249  				false,
   250  				"validatingwebhookconfigurations.admissionregistration.k8s.io",
   251  				fmt.Sprintf("e2e-%s-validating-webhook-configuration", kbc.TestSuffix),
   252  				"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
   253  			ExpectWithOffset(2, err).NotTo(HaveOccurred())
   254  			// check that ca should be long enough, because there may be a place holder "\n"
   255  			ExpectWithOffset(2, len(vwhOutput)).To(BeNumerically(">", 10))
   256  
   257  			return nil
   258  		}
   259  		EventuallyWithOffset(1, verifyCAInjection, time.Minute, time.Second).Should(Succeed())
   260  	}
   261  
   262  	By("creating an instance of the CR")
   263  	// currently controller-runtime doesn't provide a readiness probe, we retry a few times
   264  	// we can change it to probe the readiness endpoint after CR supports it.
   265  	sampleFile := filepath.Join("config", "samples",
   266  		fmt.Sprintf("%s_%s_%s.yaml", kbc.Group, kbc.Version, strings.ToLower(kbc.Kind)))
   267  
   268  	sampleFilePath, err := filepath.Abs(filepath.Join(fmt.Sprintf("e2e-%s", kbc.TestSuffix), sampleFile))
   269  	Expect(err).To(Not(HaveOccurred()))
   270  
   271  	f, err := os.OpenFile(sampleFilePath, os.O_APPEND|os.O_WRONLY, 0o644)
   272  	Expect(err).To(Not(HaveOccurred()))
   273  
   274  	defer func() {
   275  		err = f.Close()
   276  		Expect(err).To(Not(HaveOccurred()))
   277  	}()
   278  
   279  	_, err = f.WriteString("  foo: bar")
   280  	Expect(err).To(Not(HaveOccurred()))
   281  
   282  	EventuallyWithOffset(1, func() error {
   283  		_, err = kbc.Kubectl.Apply(true, "-f", sampleFile)
   284  		return err
   285  	}, time.Minute, time.Second).Should(Succeed())
   286  
   287  	By("applying the CRD Editor Role")
   288  	crdEditorRole := filepath.Join("config", "rbac",
   289  		fmt.Sprintf("%s_editor_role.yaml", strings.ToLower(kbc.Kind)))
   290  	EventuallyWithOffset(1, func() error {
   291  		_, err = kbc.Kubectl.Apply(true, "-f", crdEditorRole)
   292  		return err
   293  	}, time.Minute, time.Second).Should(Succeed())
   294  
   295  	By("applying the CRD Viewer Role")
   296  	crdViewerRole := filepath.Join("config", "rbac", fmt.Sprintf("%s_viewer_role.yaml", strings.ToLower(kbc.Kind)))
   297  	EventuallyWithOffset(1, func() error {
   298  		_, err = kbc.Kubectl.Apply(true, "-f", crdViewerRole)
   299  		return err
   300  	}, time.Minute, time.Second).Should(Succeed())
   301  
   302  	By("validating that the created resource object gets reconciled in the controller")
   303  	metricsOutput := curlMetrics(kbc)
   304  	ExpectWithOffset(1, metricsOutput).To(ContainSubstring(fmt.Sprintf(
   305  		`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
   306  		strings.ToLower(kbc.Kind),
   307  	)))
   308  
   309  	if hasWebhook {
   310  		By("validating that mutating and validating webhooks are working fine")
   311  		cnt, err := kbc.Kubectl.Get(
   312  			true,
   313  			"-f", sampleFile,
   314  			"-o", "go-template={{ .spec.count }}")
   315  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   316  		count, err := strconv.Atoi(cnt)
   317  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   318  		ExpectWithOffset(1, count).To(BeNumerically("==", 5))
   319  	}
   320  }
   321  
   322  // curlMetrics curl's the /metrics endpoint, returning all logs once a 200 status is returned.
   323  func curlMetrics(kbc *utils.TestContext) string {
   324  	By("reading the metrics token")
   325  	// Filter token query by service account in case more than one exists in a namespace.
   326  	token, err := ServiceAccountToken(kbc)
   327  	ExpectWithOffset(2, err).NotTo(HaveOccurred())
   328  	ExpectWithOffset(2, len(token)).To(BeNumerically(">", 0))
   329  
   330  	By("creating a curl pod")
   331  	cmdOpts := []string{
   332  		"run", "curl", "--image=curlimages/curl:7.68.0", "--restart=OnFailure", "--",
   333  		"curl", "-v", "-k", "-H", fmt.Sprintf(`Authorization: Bearer %s`, strings.TrimSpace(token)),
   334  		fmt.Sprintf("https://e2e-%s-controller-manager-metrics-service.%s.svc:8443/metrics",
   335  			kbc.TestSuffix, kbc.Kubectl.Namespace),
   336  	}
   337  	_, err = kbc.Kubectl.CommandInNamespace(cmdOpts...)
   338  	ExpectWithOffset(2, err).NotTo(HaveOccurred())
   339  
   340  	By("validating that the curl pod is running as expected")
   341  	verifyCurlUp := func() error {
   342  		// Validate pod status
   343  		status, err := kbc.Kubectl.Get(
   344  			true,
   345  			"pods", "curl", "-o", "jsonpath={.status.phase}")
   346  		ExpectWithOffset(3, err).NotTo(HaveOccurred())
   347  		if status != "Completed" && status != "Succeeded" {
   348  			return fmt.Errorf("curl pod in %s status", status)
   349  		}
   350  		return nil
   351  	}
   352  	EventuallyWithOffset(2, verifyCurlUp, 240*time.Second, time.Second).Should(Succeed())
   353  
   354  	By("validating that the metrics endpoint is serving as expected")
   355  	var metricsOutput string
   356  	getCurlLogs := func() string {
   357  		metricsOutput, err = kbc.Kubectl.Logs("curl")
   358  		ExpectWithOffset(3, err).NotTo(HaveOccurred())
   359  		return metricsOutput
   360  	}
   361  	EventuallyWithOffset(2, getCurlLogs, 10*time.Second, time.Second).Should(ContainSubstring("< HTTP/2 200"))
   362  
   363  	By("cleaning up the curl pod")
   364  	_, err = kbc.Kubectl.Delete(true, "pods/curl")
   365  	ExpectWithOffset(3, err).NotTo(HaveOccurred())
   366  
   367  	return metricsOutput
   368  }
   369  
   370  // ServiceAccountToken provides a helper function that can provide you with a service account
   371  // token that you can use to interact with the service. This function leverages the k8s'
   372  // TokenRequest API in raw format in order to make it generic for all version of the k8s that
   373  // is currently being supported in kubebuilder test infra.
   374  // TokenRequest API returns the token in raw JWT format itself. There is no conversion required.
   375  func ServiceAccountToken(kbc *utils.TestContext) (out string, err error) {
   376  	By("Creating the ServiceAccount token")
   377  	secretName := fmt.Sprintf("%s-token-request", kbc.Kubectl.ServiceAccount)
   378  	tokenRequestFile := filepath.Join(kbc.Dir, secretName)
   379  	err = os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o755))
   380  	if err != nil {
   381  		return out, err
   382  	}
   383  	var rawJson string
   384  	Eventually(func() error {
   385  		// Output of this is already a valid JWT token. No need to covert this from base64 to string format
   386  		rawJson, err = kbc.Kubectl.Command(
   387  			"create",
   388  			"--raw", fmt.Sprintf(
   389  				"/api/v1/namespaces/%s/serviceaccounts/%s/token",
   390  				kbc.Kubectl.Namespace,
   391  				kbc.Kubectl.ServiceAccount,
   392  			),
   393  			"-f", tokenRequestFile,
   394  		)
   395  		if err != nil {
   396  			return err
   397  		}
   398  		var token tokenRequest
   399  		err = json.Unmarshal([]byte(rawJson), &token)
   400  		if err != nil {
   401  			return err
   402  		}
   403  		out = token.Status.Token
   404  		return nil
   405  	}, time.Minute, time.Second).Should(Succeed())
   406  
   407  	return out, err
   408  }