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 }