k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/apimachinery/aggregator.go (about) 1 /* 2 Copyright 2017 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 apimachinery 18 19 import ( 20 "context" 21 "crypto/rand" 22 "encoding/json" 23 "fmt" 24 "math/big" 25 "net" 26 "strings" 27 "time" 28 29 appsv1 "k8s.io/api/apps/v1" 30 v1 "k8s.io/api/core/v1" 31 rbacv1 "k8s.io/api/rbac/v1" 32 apierrors "k8s.io/apimachinery/pkg/api/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 35 "k8s.io/apimachinery/pkg/labels" 36 "k8s.io/apimachinery/pkg/runtime/schema" 37 "k8s.io/apimachinery/pkg/types" 38 "k8s.io/apimachinery/pkg/util/intstr" 39 "k8s.io/apimachinery/pkg/util/wait" 40 "k8s.io/client-go/discovery" 41 clientset "k8s.io/client-go/kubernetes" 42 "k8s.io/client-go/util/retry" 43 apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" 44 aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" 45 rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1" 46 "k8s.io/kubernetes/test/e2e/framework" 47 e2eauth "k8s.io/kubernetes/test/e2e/framework/auth" 48 e2edeployment "k8s.io/kubernetes/test/e2e/framework/deployment" 49 e2epod "k8s.io/kubernetes/test/e2e/framework/pod" 50 "k8s.io/kubernetes/test/utils/format" 51 imageutils "k8s.io/kubernetes/test/utils/image" 52 admissionapi "k8s.io/pod-security-admission/api" 53 samplev1alpha1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1" 54 "k8s.io/utils/pointer" 55 56 "github.com/onsi/ginkgo/v2" 57 "github.com/onsi/gomega" 58 ) 59 60 const ( 61 aggregatorServicePort = 7443 62 63 apiServiceRetryPeriod = 1 * time.Second 64 apiServiceRetryTimeout = 2 * time.Minute 65 66 defaultApiServiceGroupName = samplev1alpha1.GroupName 67 defaultApiServiceVersion = "v1alpha1" 68 ) 69 70 var _ = SIGDescribe("Aggregator", func() { 71 var aggrclient *aggregatorclient.Clientset 72 73 f := framework.NewDefaultFramework("aggregator") 74 f.NamespacePodSecurityLevel = admissionapi.LevelBaseline 75 76 // We want namespace initialization BeforeEach inserted by 77 // NewDefaultFramework to happen before this, so we put this BeforeEach 78 // after NewDefaultFramework. 79 ginkgo.BeforeEach(func() { 80 config, err := framework.LoadConfig() 81 if err != nil { 82 framework.Failf("could not load config: %v", err) 83 } 84 aggrclient, err = aggregatorclient.NewForConfig(config) 85 if err != nil { 86 framework.Failf("could not create aggregator client: %v", err) 87 } 88 apiServiceName := defaultApiServiceVersion + "." + defaultApiServiceGroupName 89 ginkgo.DeferCleanup(cleanupSampleAPIServer, f.ClientSet, aggrclient, generateSampleAPIServerObjectNames(f.Namespace.Name), apiServiceName) 90 }) 91 92 /* 93 Release: v1.17, v1.21, v1.27 94 Testname: aggregator-supports-the-sample-apiserver 95 Description: Ensure that the sample-apiserver code from 1.17 and compiled against 1.17 96 will work on the current Aggregator/API-Server. 97 */ 98 framework.ConformanceIt("Should be able to support the 1.17 Sample API Server using the current Aggregator", func(ctx context.Context) { 99 // Testing a 1.17 version of the sample-apiserver 100 TestSampleAPIServer(ctx, f, aggrclient, imageutils.GetE2EImage(imageutils.APIServer), defaultApiServiceGroupName, defaultApiServiceVersion) 101 }) 102 }) 103 104 func cleanupSampleAPIServer(ctx context.Context, client clientset.Interface, aggrclient *aggregatorclient.Clientset, n sampleAPIServerObjectNames, apiServiceName string) { 105 // delete the APIService first to avoid causing discovery errors 106 _ = aggrclient.ApiregistrationV1().APIServices().Delete(ctx, apiServiceName, metav1.DeleteOptions{}) 107 108 _ = client.AppsV1().Deployments(n.namespace).Delete(ctx, "sample-apiserver-deployment", metav1.DeleteOptions{}) 109 _ = client.CoreV1().Secrets(n.namespace).Delete(ctx, "sample-apiserver-secret", metav1.DeleteOptions{}) 110 _ = client.CoreV1().Services(n.namespace).Delete(ctx, "sample-api", metav1.DeleteOptions{}) 111 _ = client.CoreV1().ServiceAccounts(n.namespace).Delete(ctx, "sample-apiserver", metav1.DeleteOptions{}) 112 _ = client.RbacV1().RoleBindings("kube-system").Delete(ctx, n.roleBinding, metav1.DeleteOptions{}) 113 _ = client.RbacV1().ClusterRoleBindings().Delete(ctx, "wardler:"+n.namespace+":auth-delegator", metav1.DeleteOptions{}) 114 _ = client.RbacV1().ClusterRoles().Delete(ctx, n.clusterRole, metav1.DeleteOptions{}) 115 _ = client.RbacV1().ClusterRoleBindings().Delete(ctx, n.clusterRoleBinding, metav1.DeleteOptions{}) 116 } 117 118 type sampleAPIServerObjectNames struct { 119 namespace string 120 roleBinding string 121 clusterRole string 122 clusterRoleBinding string 123 } 124 125 func generateSampleAPIServerObjectNames(namespace string) sampleAPIServerObjectNames { 126 return sampleAPIServerObjectNames{ 127 namespace: namespace, 128 roleBinding: "wardler-auth-reader-" + namespace, 129 clusterRole: "sample-apiserver-reader-" + namespace, 130 clusterRoleBinding: "wardler:" + namespace + "sample-apiserver-reader-" + namespace, 131 } 132 } 133 134 func SetUpSampleAPIServer(ctx context.Context, f *framework.Framework, aggrclient *aggregatorclient.Clientset, image string, n sampleAPIServerObjectNames, apiServiceGroupName, apiServiceVersion string) { 135 ginkgo.By("Registering the sample API server.") 136 client := f.ClientSet 137 restClient := client.Discovery().RESTClient() 138 certCtx := setupServerCert(n.namespace, "sample-api") 139 apiServiceName := apiServiceVersion + "." + apiServiceGroupName 140 141 // kubectl create -f namespace.yaml 142 // NOTE: aggregated apis should generally be set up in their own namespace. As the test framework is setting up a new namespace, we are just using that. 143 144 // kubectl create -f secret.yaml 145 secretName := "sample-apiserver-secret" 146 secret := &v1.Secret{ 147 ObjectMeta: metav1.ObjectMeta{ 148 Name: secretName, 149 }, 150 Type: v1.SecretTypeOpaque, 151 Data: map[string][]byte{ 152 "tls.crt": certCtx.cert, 153 "tls.key": certCtx.key, 154 }, 155 } 156 _, err := client.CoreV1().Secrets(n.namespace).Create(ctx, secret, metav1.CreateOptions{}) 157 framework.ExpectNoError(err, "creating secret %s in.namespace %s", secretName, n.namespace) 158 159 if e2eauth.IsRBACEnabled(ctx, client.RbacV1()) { 160 // kubectl create -f clusterrole.yaml 161 _, err = client.RbacV1().ClusterRoles().Create(ctx, &rbacv1.ClusterRole{ 162 163 ObjectMeta: metav1.ObjectMeta{Name: n.clusterRole}, 164 Rules: []rbacv1.PolicyRule{ 165 rbacv1helpers.NewRule("get", "list", "watch").Groups("").Resources("namespaces").RuleOrDie(), 166 rbacv1helpers.NewRule("get", "list", "watch").Groups("admissionregistration.k8s.io").Resources("*").RuleOrDie(), 167 rbacv1helpers.NewRule("get", "list", "watch").Groups("flowcontrol.apiserver.k8s.io").Resources("prioritylevelconfigurations", "flowschemas").RuleOrDie(), 168 }, 169 }, metav1.CreateOptions{}) 170 framework.ExpectNoError(err, "creating cluster role %s", n.clusterRole) 171 172 _, err = client.RbacV1().ClusterRoleBindings().Create(ctx, &rbacv1.ClusterRoleBinding{ 173 ObjectMeta: metav1.ObjectMeta{ 174 Name: n.clusterRoleBinding, 175 }, 176 RoleRef: rbacv1.RoleRef{ 177 APIGroup: "rbac.authorization.k8s.io", 178 Kind: "ClusterRole", 179 Name: n.clusterRole, 180 }, 181 Subjects: []rbacv1.Subject{ 182 { 183 APIGroup: "", 184 Kind: "ServiceAccount", 185 Name: "default", 186 Namespace: n.namespace, 187 }, 188 }, 189 }, metav1.CreateOptions{}) 190 framework.ExpectNoError(err, "creating cluster role binding %s", n.clusterRoleBinding) 191 192 // kubectl create -f authDelegator.yaml 193 _, err = client.RbacV1().ClusterRoleBindings().Create(ctx, &rbacv1.ClusterRoleBinding{ 194 ObjectMeta: metav1.ObjectMeta{ 195 Name: "wardler:" + n.namespace + ":auth-delegator", 196 }, 197 RoleRef: rbacv1.RoleRef{ 198 APIGroup: "rbac.authorization.k8s.io", 199 Kind: "ClusterRole", 200 Name: "system:auth-delegator", 201 }, 202 Subjects: []rbacv1.Subject{ 203 { 204 APIGroup: "", 205 Kind: "ServiceAccount", 206 Name: "default", 207 Namespace: n.namespace, 208 }, 209 }, 210 }, metav1.CreateOptions{}) 211 framework.ExpectNoError(err, "creating cluster role binding %s", "wardler:"+n.namespace+":auth-delegator") 212 } 213 214 // kubectl create -f deploy.yaml 215 deploymentName := "sample-apiserver-deployment" 216 etcdImage := imageutils.GetE2EImage(imageutils.Etcd) 217 podLabels := map[string]string{"app": "sample-apiserver", "apiserver": "true"} 218 replicas := int32(1) 219 etcdLocalhostAddress := "127.0.0.1" 220 if framework.TestContext.ClusterIsIPv6() { 221 etcdLocalhostAddress = "::1" 222 } 223 etcdURL := fmt.Sprintf("http://%s", net.JoinHostPort(etcdLocalhostAddress, "2379")) 224 225 mounts := []v1.VolumeMount{ 226 { 227 Name: "apiserver-certs", 228 ReadOnly: true, 229 MountPath: "/apiserver.local.config/certificates", 230 }, 231 } 232 volumes := []v1.Volume{ 233 { 234 Name: "apiserver-certs", 235 VolumeSource: v1.VolumeSource{ 236 Secret: &v1.SecretVolumeSource{SecretName: secretName}, 237 }, 238 }, 239 } 240 containers := []v1.Container{ 241 { 242 Name: "sample-apiserver", 243 VolumeMounts: mounts, 244 Args: []string{ 245 fmt.Sprintf("--etcd-servers=%s", etcdURL), 246 "--tls-cert-file=/apiserver.local.config/certificates/tls.crt", 247 "--tls-private-key-file=/apiserver.local.config/certificates/tls.key", 248 "--audit-log-path=-", 249 "--audit-log-maxage=0", 250 "--audit-log-maxbackup=0", 251 }, 252 Image: image, 253 ReadinessProbe: &v1.Probe{ 254 ProbeHandler: v1.ProbeHandler{ 255 HTTPGet: &v1.HTTPGetAction{ 256 Scheme: v1.URISchemeHTTPS, 257 Port: intstr.FromInt32(443), 258 Path: "/readyz", 259 }, 260 }, 261 InitialDelaySeconds: 20, 262 PeriodSeconds: 1, 263 SuccessThreshold: 1, 264 FailureThreshold: 3, 265 }, 266 }, 267 { 268 Name: "etcd", 269 Image: etcdImage, 270 Command: []string{ 271 "/usr/local/bin/etcd", 272 "--listen-client-urls", 273 etcdURL, 274 "--advertise-client-urls", 275 etcdURL, 276 }, 277 }, 278 } 279 d := e2edeployment.NewDeployment(deploymentName, replicas, podLabels, "", "", appsv1.RollingUpdateDeploymentStrategyType) 280 d.Spec.Template.Spec.Containers = containers 281 d.Spec.Template.Spec.Volumes = volumes 282 283 deployment, err := client.AppsV1().Deployments(n.namespace).Create(ctx, d, metav1.CreateOptions{}) 284 framework.ExpectNoError(err, "creating deployment %s in namespace %s", deploymentName, n.namespace) 285 286 err = e2edeployment.WaitForDeploymentRevisionAndImage(client, n.namespace, deploymentName, "1", image) 287 framework.ExpectNoError(err, "waiting for the deployment of image %s in %s in %s to complete", image, deploymentName, n.namespace) 288 289 err = e2edeployment.WaitForDeploymentRevisionAndImage(client, n.namespace, deploymentName, "1", etcdImage) 290 framework.ExpectNoError(err, "waiting for the deployment of image %s in %s in %s to complete", etcdImage, deploymentName, n.namespace) 291 292 // kubectl create -f service.yaml 293 serviceLabels := map[string]string{"apiserver": "true"} 294 service := &v1.Service{ 295 ObjectMeta: metav1.ObjectMeta{ 296 Namespace: n.namespace, 297 Name: "sample-api", 298 Labels: map[string]string{"test": "aggregator"}, 299 }, 300 Spec: v1.ServiceSpec{ 301 Selector: serviceLabels, 302 Ports: []v1.ServicePort{ 303 { 304 Protocol: v1.ProtocolTCP, 305 Port: aggregatorServicePort, 306 TargetPort: intstr.FromInt32(443), 307 }, 308 }, 309 }, 310 } 311 _, err = client.CoreV1().Services(n.namespace).Create(ctx, service, metav1.CreateOptions{}) 312 framework.ExpectNoError(err, "creating service %s in namespace %s", "sample-api", n.namespace) 313 314 // kubectl create -f serviceAccount.yaml 315 sa := &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "sample-apiserver"}} 316 _, err = client.CoreV1().ServiceAccounts(n.namespace).Create(ctx, sa, metav1.CreateOptions{}) 317 framework.ExpectNoError(err, "creating service account %s in namespace %s", "sample-apiserver", n.namespace) 318 319 if e2eauth.IsRBACEnabled(ctx, client.RbacV1()) { 320 // kubectl create -f auth-reader.yaml 321 _, err = client.RbacV1().RoleBindings("kube-system").Create(ctx, &rbacv1.RoleBinding{ 322 ObjectMeta: metav1.ObjectMeta{ 323 Name: n.roleBinding, 324 Annotations: map[string]string{ 325 rbacv1.AutoUpdateAnnotationKey: "true", 326 }, 327 }, 328 RoleRef: rbacv1.RoleRef{ 329 APIGroup: "", 330 Kind: "Role", 331 Name: "extension-apiserver-authentication-reader", 332 }, 333 Subjects: []rbacv1.Subject{ 334 { 335 Kind: "ServiceAccount", 336 Name: "default", 337 Namespace: n.namespace, 338 }, 339 }, 340 }, metav1.CreateOptions{}) 341 framework.ExpectNoError(err, "creating role binding %s in namespace %s", n.roleBinding, "kube-system") 342 } 343 344 // Wait for the extension apiserver to be up and healthy 345 // kubectl get deployments -n <aggregated-api-namespace> && status == Running 346 // NOTE: aggregated apis should generally be set up in their own namespace (<aggregated-api-namespace>). As the test framework 347 // is setting up a new namespace, we are just using that. 348 err = e2edeployment.WaitForDeploymentComplete(client, deployment) 349 framework.ExpectNoError(err, "deploying extension apiserver in namespace %s", n.namespace) 350 351 // kubectl create -f apiservice.yaml 352 _, err = aggrclient.ApiregistrationV1().APIServices().Create(ctx, &apiregistrationv1.APIService{ 353 ObjectMeta: metav1.ObjectMeta{Name: apiServiceName}, 354 Spec: apiregistrationv1.APIServiceSpec{ 355 Service: &apiregistrationv1.ServiceReference{ 356 Namespace: n.namespace, 357 Name: "sample-api", 358 Port: pointer.Int32(aggregatorServicePort), 359 }, 360 Group: apiServiceGroupName, 361 Version: apiServiceVersion, 362 CABundle: certCtx.signingCert, 363 GroupPriorityMinimum: 2000, 364 VersionPriority: 200, 365 }, 366 }, metav1.CreateOptions{}) 367 framework.ExpectNoError(err, "creating apiservice %s", apiServiceName) 368 369 var ( 370 currentAPIService *apiregistrationv1.APIService 371 currentPods *v1.PodList 372 ) 373 374 err = pollTimed(ctx, 100*time.Millisecond, 60*time.Second, func(ctx context.Context) (bool, error) { 375 376 currentAPIService, _ = aggrclient.ApiregistrationV1().APIServices().Get(ctx, apiServiceName, metav1.GetOptions{}) 377 currentPods, _ = client.CoreV1().Pods(n.namespace).List(ctx, metav1.ListOptions{}) 378 379 request := restClient.Get().AbsPath("/apis/" + apiServiceGroupName + "/" + apiServiceVersion + "/namespaces/default/flunders") 380 request.SetHeader("Accept", "application/json") 381 _, err := request.DoRaw(ctx) 382 if err != nil { 383 status, ok := err.(*apierrors.StatusError) 384 if !ok { 385 return false, err 386 } 387 if status.Status().Code == 403 || status.Status().Code == 503 { 388 return false, nil 389 } 390 if status.Status().Code == 404 && strings.HasPrefix(err.Error(), "the server could not find the requested resource") { 391 return false, nil 392 } 393 return false, err 394 } 395 return true, nil 396 }, "Waited %s for the sample-apiserver to be ready to handle requests.") 397 if err != nil { 398 currentAPIServiceJSON, _ := json.Marshal(currentAPIService) 399 framework.Logf("current APIService: %s", string(currentAPIServiceJSON)) 400 401 currentPodsJSON, _ := json.Marshal(currentPods) 402 framework.Logf("current pods: %s", string(currentPodsJSON)) 403 404 if currentPods != nil { 405 for _, pod := range currentPods.Items { 406 for _, container := range pod.Spec.Containers { 407 logs, err := e2epod.GetPodLogs(ctx, client, n.namespace, pod.Name, container.Name) 408 framework.Logf("logs of %s/%s (error: %v): %s", pod.Name, container.Name, err, logs) 409 } 410 } 411 } 412 } 413 framework.ExpectNoError(err, "gave up waiting for apiservice wardle to come up successfully") 414 } 415 416 // TestSampleAPIServer is a basic test if the sample-apiserver code from 1.29 and compiled against 1.29 417 // will work on the current Aggregator/API-Server. 418 func TestSampleAPIServer(ctx context.Context, f *framework.Framework, aggrclient *aggregatorclient.Clientset, image, apiServiceGroupName, apiServiceVersion string) { 419 n := generateSampleAPIServerObjectNames(f.Namespace.Name) 420 SetUpSampleAPIServer(ctx, f, aggrclient, image, n, apiServiceGroupName, apiServiceVersion) 421 client := f.ClientSet 422 restClient := client.Discovery().RESTClient() 423 424 flunderName := generateFlunderName("rest-flunder") 425 apiServiceName := apiServiceVersion + "." + apiServiceGroupName 426 427 // kubectl create -f flunders-1.yaml -v 9 428 // curl -k -v -XPOST https://localhost/apis/wardle.example.com/v1alpha1/namespaces/default/flunders 429 // Request Body: {"apiVersion":"wardle.example.com/v1alpha1","kind":"Flunder","metadata":{"labels":{"sample-label":"true"},"name":"test-flunder","namespace":"default"}} 430 flunder := `{"apiVersion":"` + apiServiceGroupName + `/` + apiServiceVersion + `","kind":"Flunder","metadata":{"labels":{"sample-label":"true"},"name":"` + flunderName + `","namespace":"default"}}` 431 result := restClient.Post().AbsPath("/apis/"+apiServiceGroupName+"/"+apiServiceVersion+"/namespaces/default/flunders").Body([]byte(flunder)).SetHeader("Accept", "application/json").Do(ctx) 432 framework.ExpectNoError(result.Error(), "creating a new flunders resource") 433 var statusCode int 434 result.StatusCode(&statusCode) 435 if statusCode != 201 { 436 framework.Failf("Flunders client creation response was status %d, not 201", statusCode) 437 } 438 u := &unstructured.Unstructured{} 439 if err := result.Into(u); err != nil { 440 framework.ExpectNoError(err, "reading created response") 441 } 442 443 gomega.Expect(u.GetAPIVersion()).To(gomega.Equal(apiServiceGroupName + "/" + apiServiceVersion)) 444 gomega.Expect(u.GetKind()).To(gomega.Equal("Flunder")) 445 gomega.Expect(u.GetName()).To(gomega.Equal(flunderName)) 446 447 pods, err := client.CoreV1().Pods(n.namespace).List(ctx, metav1.ListOptions{}) 448 framework.ExpectNoError(err, "getting pods for flunders service") 449 450 // kubectl get flunders -v 9 451 // curl -k -v -XGET https://localhost/apis/wardle.example.com/v1alpha1/namespaces/default/flunders 452 contents, err := restClient.Get().AbsPath("/apis/"+apiServiceGroupName+"/"+apiServiceVersion+"/namespaces/default/flunders").SetHeader("Accept", "application/json").DoRaw(ctx) 453 framework.ExpectNoError(err, "attempting to get a newly created flunders resource") 454 var flundersList samplev1alpha1.FlunderList 455 err = json.Unmarshal(contents, &flundersList) 456 validateErrorWithDebugInfo(ctx, f, err, pods, "Error in unmarshalling %T response from server %s", contents, "/apis/"+apiServiceGroupName+"/"+apiServiceVersion) 457 if len(flundersList.Items) != 1 { 458 framework.Failf("failed to get back the correct flunders list %v", flundersList) 459 } 460 461 // kubectl delete flunder test-flunder -v 9 462 // curl -k -v -XDELETE https://35.193.112.40/apis/wardle.example.com/v1alpha1/namespaces/default/flunders/test-flunder 463 _, err = restClient.Delete().AbsPath("/apis/" + apiServiceGroupName + "/" + apiServiceVersion + "/namespaces/default/flunders/" + flunderName).DoRaw(ctx) 464 validateErrorWithDebugInfo(ctx, f, err, pods, "attempting to delete a newly created flunders(%v) resource", flundersList.Items) 465 466 // kubectl get flunders -v 9 467 // curl -k -v -XGET https://localhost/apis/wardle.example.com/v1alpha1/namespaces/default/flunders 468 contents, err = restClient.Get().AbsPath("/apis/"+apiServiceGroupName+"/"+apiServiceVersion+"/namespaces/default/flunders").SetHeader("Accept", "application/json").DoRaw(ctx) 469 framework.ExpectNoError(err, "confirming delete of a newly created flunders resource") 470 err = json.Unmarshal(contents, &flundersList) 471 validateErrorWithDebugInfo(ctx, f, err, pods, "Error in unmarshalling %T response from server %s", contents, "/apis/"+apiServiceGroupName+"/"+apiServiceVersion) 472 if len(flundersList.Items) != 0 { 473 framework.Failf("failed to get back the correct deleted flunders list %v", flundersList) 474 } 475 476 flunderName = generateFlunderName("dynamic-flunder") 477 478 // Rerun the Create/List/Delete tests using the Dynamic client. 479 resources, discoveryErr := client.Discovery().ServerPreferredNamespacedResources() 480 groupVersionResources, err := discovery.GroupVersionResources(resources) 481 framework.ExpectNoError(err, "getting group version resources for dynamic client") 482 gvr := schema.GroupVersionResource{Group: apiServiceGroupName, Version: apiServiceVersion, Resource: "flunders"} 483 _, ok := groupVersionResources[gvr] 484 if !ok { 485 framework.Failf("could not find group version resource for dynamic client and wardle/flunders (discovery error: %v, discovery results: %#v)", discoveryErr, groupVersionResources) 486 } 487 dynamicClient := f.DynamicClient.Resource(gvr).Namespace(n.namespace) 488 489 // kubectl create -f flunders-1.yaml 490 // Request Body: {"apiVersion":"wardle.example.com/v1alpha1","kind":"Flunder","metadata":{"labels":{"sample-label":"true"},"name":"test-flunder","namespace":"default"}} 491 testFlunder := samplev1alpha1.Flunder{ 492 TypeMeta: metav1.TypeMeta{ 493 Kind: "Flunder", 494 APIVersion: apiServiceGroupName + "/" + apiServiceVersion, 495 }, 496 ObjectMeta: metav1.ObjectMeta{Name: flunderName}, 497 Spec: samplev1alpha1.FlunderSpec{}, 498 } 499 jsonFlunder, err := json.Marshal(testFlunder) 500 framework.ExpectNoError(err, "marshalling test-flunder for create using dynamic client") 501 unstruct := &unstructured.Unstructured{} 502 err = unstruct.UnmarshalJSON(jsonFlunder) 503 framework.ExpectNoError(err, "unmarshalling test-flunder as unstructured for create using dynamic client") 504 _, err = dynamicClient.Create(ctx, unstruct, metav1.CreateOptions{}) 505 framework.ExpectNoError(err, "listing flunders using dynamic client") 506 507 // kubectl get flunders 508 unstructuredList, err := dynamicClient.List(ctx, metav1.ListOptions{}) 509 framework.ExpectNoError(err, "listing flunders using dynamic client") 510 if len(unstructuredList.Items) != 1 { 511 framework.Failf("failed to get back the correct flunders list %v from the dynamic client", unstructuredList) 512 } 513 514 ginkgo.By("Read Status for " + apiServiceName) 515 statusContent, err := restClient.Get(). 516 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices/"+apiServiceName+"/status"). 517 SetHeader("Accept", "application/json").DoRaw(ctx) 518 framework.ExpectNoError(err, "No response for .../apiservices/"+apiServiceName+"/status. Error: %v", err) 519 520 var jr *apiregistrationv1.APIService 521 err = json.Unmarshal([]byte(statusContent), &jr) 522 framework.ExpectNoError(err, "Failed to process statusContent: %v | err: %v ", string(statusContent), err) 523 gomega.Expect(jr.Status.Conditions[0].Message).To(gomega.Equal("all checks passed"), "The Message returned was %v", jr.Status.Conditions[0].Message) 524 525 ginkgo.By("kubectl patch apiservice " + apiServiceName + " -p '{\"spec\":{\"versionPriority\": 400}}'") 526 patchContent, err := restClient.Patch(types.MergePatchType). 527 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices/"+apiServiceName). 528 SetHeader("Accept", "application/json"). 529 Body([]byte(`{"spec":{"versionPriority": 400}}`)).DoRaw(ctx) 530 531 framework.ExpectNoError(err, "Patch failed for .../apiservices/"+apiServiceName+". Error: %v", err) 532 err = json.Unmarshal([]byte(patchContent), &jr) 533 framework.ExpectNoError(err, "Failed to process patchContent: %v | err: %v ", string(patchContent), err) 534 gomega.Expect(jr.Spec.VersionPriority).To(gomega.Equal(int32(400)), "The VersionPriority returned was %d", jr.Spec.VersionPriority) 535 536 ginkgo.By("List APIServices") 537 listApiservices, err := restClient.Get(). 538 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices"). 539 SetHeader("Accept", "application/json").DoRaw(ctx) 540 541 framework.ExpectNoError(err, "No response for /apis/apiregistration.k8s.io/v1/apiservices Error: %v", err) 542 543 var list *apiregistrationv1.APIServiceList 544 err = json.Unmarshal([]byte(listApiservices), &list) 545 framework.ExpectNoError(err, "Failed to process APIServiceList: %v | err: %v ", list, err) 546 547 locatedWardle := false 548 for _, item := range list.Items { 549 if item.Name == apiServiceName { 550 framework.Logf("Found " + apiServiceName + " in APIServiceList") 551 locatedWardle = true 552 break 553 } 554 } 555 if !locatedWardle { 556 framework.Failf("Unable to find " + apiServiceName + " in APIServiceList") 557 } 558 559 // As the APIService doesn't have any labels currently set we need to 560 // set one so that we can select it later when we call deleteCollection 561 ginkgo.By("Adding a label to the APIService") 562 apiServiceClient := aggrclient.ApiregistrationV1().APIServices() 563 apiServiceLabel := map[string]string{"e2e-apiservice": "patched"} 564 apiServicePatch, err := json.Marshal(map[string]interface{}{ 565 "metadata": map[string]interface{}{ 566 "labels": apiServiceLabel, 567 }, 568 }) 569 framework.ExpectNoError(err, "failed to Marshal APIService JSON patch") 570 _, err = apiServiceClient.Patch(ctx, apiServiceName, types.StrategicMergePatchType, []byte(apiServicePatch), metav1.PatchOptions{}) 571 framework.ExpectNoError(err, "failed to patch APIService") 572 573 patchedApiService, err := apiServiceClient.Get(ctx, apiServiceName, metav1.GetOptions{}) 574 framework.ExpectNoError(err, "Unable to retrieve api service %s", apiServiceName) 575 framework.Logf("APIService labels: %v", patchedApiService.Labels) 576 577 ginkgo.By("Updating APIService Status") 578 var updatedStatus, wardle *apiregistrationv1.APIService 579 580 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 581 var statusToUpdate *apiregistrationv1.APIService 582 statusContent, err = restClient.Get(). 583 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices/"+apiServiceName+"/status"). 584 SetHeader("Accept", "application/json").DoRaw(ctx) 585 framework.ExpectNoError(err, "No response for .../apiservices/"+apiServiceName+"/status. Error: %v", err) 586 587 err = json.Unmarshal([]byte(statusContent), &statusToUpdate) 588 framework.ExpectNoError(err, "Failed to process statusContent: %v | err: %v ", string(statusContent), err) 589 590 statusToUpdate.Status.Conditions = append(statusToUpdate.Status.Conditions, apiregistrationv1.APIServiceCondition{ 591 Type: "StatusUpdated", 592 Status: "True", 593 Reason: "E2E", 594 Message: "Set from e2e test", 595 }) 596 597 updatedStatus, err = apiServiceClient.UpdateStatus(ctx, statusToUpdate, metav1.UpdateOptions{}) 598 return err 599 }) 600 framework.ExpectNoError(err, "Failed to update status. %v", err) 601 framework.Logf("updatedStatus.Conditions: %#v", updatedStatus.Status.Conditions) 602 603 ginkgo.By("Confirm that " + apiServiceName + " /status was updated") 604 statusContent, err = restClient.Get(). 605 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices/"+apiServiceName+"/status"). 606 SetHeader("Accept", "application/json").DoRaw(ctx) 607 framework.ExpectNoError(err, "No response for .../apiservices/"+apiServiceName+"/status. Error: %v", err) 608 609 err = json.Unmarshal([]byte(statusContent), &wardle) 610 framework.ExpectNoError(err, "Failed to process statusContent: %v | err: %v ", string(statusContent), err) 611 612 foundUpdatedStatusCondition := false 613 for _, cond := range wardle.Status.Conditions { 614 if cond.Type == "StatusUpdated" && cond.Reason == "E2E" && cond.Message == "Set from e2e test" { 615 framework.Logf("Found APIService %v with Labels: %v & Condition: %v", wardle.ObjectMeta.Name, wardle.Labels, cond) 616 foundUpdatedStatusCondition = true 617 break 618 } else { 619 framework.Logf("Observed APIService %v with Labels: %v & Condition: %v", wardle.ObjectMeta.Name, wardle.Labels, cond) 620 } 621 } 622 if !foundUpdatedStatusCondition { 623 framework.Failf("The updated status condition was not found in:\n%s", format.Object(wardle.Status.Conditions, 1)) 624 } 625 framework.Logf("Found updated status condition for %s", wardle.ObjectMeta.Name) 626 627 ginkgo.By(fmt.Sprintf("Replace APIService %s", apiServiceName)) 628 var updatedApiService *apiregistrationv1.APIService 629 630 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 631 currentApiService, err := apiServiceClient.Get(ctx, apiServiceName, metav1.GetOptions{}) 632 framework.ExpectNoError(err, "Unable to get APIService %s", apiServiceName) 633 currentApiService.Labels = map[string]string{ 634 apiServiceName: "updated", 635 } 636 updatedApiService, err = apiServiceClient.Update(ctx, currentApiService, metav1.UpdateOptions{}) 637 return err 638 }) 639 framework.ExpectNoError(err) 640 gomega.Expect(updatedApiService.Labels).To(gomega.HaveKeyWithValue(apiServiceName, "updated"), "should have the updated label but have %q", updatedApiService.Labels[apiServiceName]) 641 framework.Logf("Found updated apiService label for %q", apiServiceName) 642 643 // kubectl delete flunder test-flunder 644 ginkgo.By(fmt.Sprintf("Delete flunders resource %q", flunderName)) 645 err = dynamicClient.Delete(ctx, flunderName, metav1.DeleteOptions{}) 646 validateErrorWithDebugInfo(ctx, f, err, pods, "deleting flunders(%v) using dynamic client", unstructuredList.Items) 647 648 // kubectl get flunders 649 unstructuredList, err = dynamicClient.List(ctx, metav1.ListOptions{}) 650 framework.ExpectNoError(err, "listing flunders using dynamic client") 651 if len(unstructuredList.Items) != 0 { 652 framework.Failf("failed to get back the correct deleted flunders list %v from the dynamic client", unstructuredList) 653 } 654 655 ginkgo.By("Recreating test-flunder before removing endpoint via deleteCollection") 656 jsonFlunder, err = json.Marshal(testFlunder) 657 framework.ExpectNoError(err, "marshalling test-flunder for create using dynamic client") 658 unstruct = &unstructured.Unstructured{} 659 err = unstruct.UnmarshalJSON(jsonFlunder) 660 framework.ExpectNoError(err, "unmarshalling test-flunder as unstructured for create using dynamic client") 661 _, err = dynamicClient.Create(ctx, unstruct, metav1.CreateOptions{}) 662 framework.ExpectNoError(err, "listing flunders using dynamic client") 663 664 // kubectl get flunders 665 unstructuredList, err = dynamicClient.List(ctx, metav1.ListOptions{}) 666 framework.ExpectNoError(err, "listing flunders using dynamic client") 667 if len(unstructuredList.Items) != 1 { 668 framework.Failf("failed to get back the correct flunders list %v from the dynamic client", unstructuredList) 669 } 670 671 ginkgo.By("Read " + apiServiceName + " /status before patching it") 672 statusContent, err = restClient.Get(). 673 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices/"+apiServiceName+"/status"). 674 SetHeader("Accept", "application/json").DoRaw(ctx) 675 framework.ExpectNoError(err, "No response for .../apiservices/"+apiServiceName+"/status. Error: %v", err) 676 677 wardle.Reset() 678 err = json.Unmarshal([]byte(statusContent), &wardle) 679 framework.ExpectNoError(err, "Failed to process statusContent: %v | err: %v ", string(statusContent), err) 680 681 ginkgo.By("Patch APIService Status") 682 patch := map[string]interface{}{ 683 "status": map[string]interface{}{ 684 "conditions": append(wardle.Status.Conditions, apiregistrationv1.APIServiceCondition{ 685 Type: "StatusPatched", 686 Status: "True", 687 Reason: "E2E", 688 Message: "Set by e2e test", 689 }), 690 }, 691 } 692 payload, err := json.Marshal(patch) 693 framework.ExpectNoError(err, "Failed to marshal JSON. %v", err) 694 695 _, err = restClient.Patch(types.MergePatchType). 696 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices/"+apiServiceName+"/status"). 697 SetHeader("Accept", "application/json"). 698 Body([]byte(payload)). 699 DoRaw(ctx) 700 framework.ExpectNoError(err, "Patch failed for .../apiservices/"+apiServiceName+"/status. Error: %v", err) 701 702 ginkgo.By("Confirm that " + apiServiceName + " /status was patched") 703 statusContent, err = restClient.Get(). 704 AbsPath("/apis/apiregistration.k8s.io/v1/apiservices/"+apiServiceName+"/status"). 705 SetHeader("Accept", "application/json").DoRaw(ctx) 706 framework.ExpectNoError(err, "No response for .../apiservices/"+apiServiceName+"/status. Error: %v", err) 707 708 wardle.Reset() 709 err = json.Unmarshal([]byte(statusContent), &wardle) 710 framework.ExpectNoError(err, "Failed to process statusContent: %v | err: %v ", string(statusContent), err) 711 712 foundPatchedStatusCondition := false 713 for _, cond := range wardle.Status.Conditions { 714 if cond.Type == "StatusPatched" && cond.Reason == "E2E" && cond.Message == "Set by e2e test" { 715 framework.Logf("Found APIService %v with Labels: %v & Conditions: %v", wardle.ObjectMeta.Name, wardle.Labels, cond) 716 foundPatchedStatusCondition = true 717 break 718 } else { 719 framework.Logf("Observed APIService %v with Labels: %v & Conditions: %v", wardle.ObjectMeta.Name, wardle.Labels, cond) 720 } 721 } 722 if !foundPatchedStatusCondition { 723 framework.Failf("The patched status condition was not found in:\n%s", format.Object(wardle.Status.Conditions, 1)) 724 } 725 framework.Logf("Found patched status condition for %s", wardle.ObjectMeta.Name) 726 727 apiServiceLabelSelector := labels.SelectorFromSet(updatedApiService.Labels).String() 728 ginkgo.By(fmt.Sprintf("APIService deleteCollection with labelSelector: %q", apiServiceLabelSelector)) 729 730 err = aggrclient.ApiregistrationV1().APIServices().DeleteCollection(ctx, 731 metav1.DeleteOptions{}, 732 metav1.ListOptions{LabelSelector: apiServiceLabelSelector}) 733 framework.ExpectNoError(err, "Unable to delete apiservice %s", apiServiceName) 734 735 ginkgo.By("Confirm that the generated APIService has been deleted") 736 err = wait.PollImmediate(apiServiceRetryPeriod, apiServiceRetryTimeout, checkApiServiceListQuantity(ctx, aggrclient, apiServiceLabelSelector, 0)) 737 framework.ExpectNoError(err, "failed to count the required APIServices") 738 framework.Logf("APIService %s has been deleted.", apiServiceName) 739 740 cleanupSampleAPIServer(ctx, client, aggrclient, n, apiServiceName) 741 } 742 743 // pollTimed will call Poll but time how long Poll actually took. 744 // It will then framework.Logf the msg with the duration of the Poll. 745 // It is assumed that msg will contain one %s for the elapsed time. 746 func pollTimed(ctx context.Context, interval, timeout time.Duration, condition wait.ConditionWithContextFunc, msg string) error { 747 defer func(start time.Time, msg string) { 748 elapsed := time.Since(start) 749 framework.Logf(msg, elapsed) 750 }(time.Now(), msg) 751 return wait.PollWithContext(ctx, interval, timeout, condition) 752 } 753 754 func validateErrorWithDebugInfo(ctx context.Context, f *framework.Framework, err error, pods *v1.PodList, msg string, fields ...interface{}) { 755 if err != nil { 756 namespace := f.Namespace.Name 757 msg := fmt.Sprintf(msg, fields...) 758 msg += fmt.Sprintf(" but received unexpected error:\n%v", err) 759 client := f.ClientSet 760 ep, err := client.CoreV1().Endpoints(namespace).Get(ctx, "sample-api", metav1.GetOptions{}) 761 if err == nil { 762 msg += fmt.Sprintf("\nFound endpoints for sample-api:\n%v", ep) 763 } 764 pds, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) 765 if err == nil { 766 msg += fmt.Sprintf("\nFound pods in %s:\n%v", namespace, pds) 767 msg += fmt.Sprintf("\nOriginal pods in %s:\n%v", namespace, pods) 768 } 769 770 framework.Failf(msg) 771 } 772 } 773 774 func generateFlunderName(base string) string { 775 id, err := rand.Int(rand.Reader, big.NewInt(2147483647)) 776 if err != nil { 777 return base 778 } 779 return fmt.Sprintf("%s-%d", base, id) 780 } 781 782 func checkApiServiceListQuantity(ctx context.Context, aggrclient *aggregatorclient.Clientset, label string, quantity int) func() (bool, error) { 783 return func() (bool, error) { 784 var err error 785 786 framework.Logf("Requesting list of APIServices to confirm quantity") 787 788 list, err := aggrclient.ApiregistrationV1().APIServices().List(ctx, metav1.ListOptions{LabelSelector: label}) 789 if err != nil { 790 return false, err 791 } 792 793 if len(list.Items) != quantity { 794 return false, err 795 } 796 framework.Logf("Found %d APIService with label %q", quantity, label) 797 return true, nil 798 } 799 }