k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/apimachinery/crd_selectable_fields.go (about)

     1  /*
     2  Copyright 2024 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  	"fmt"
    22  	"github.com/onsi/gomega"
    23  	"time"
    24  
    25  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    26  	apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
    27  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/apimachinery/pkg/watch"
    34  	"k8s.io/apiserver/pkg/storage/names"
    35  	"k8s.io/client-go/dynamic"
    36  	"k8s.io/kubernetes/test/e2e/framework"
    37  	"k8s.io/kubernetes/test/utils/crd"
    38  	imageutils "k8s.io/kubernetes/test/utils/image"
    39  	admissionapi "k8s.io/pod-security-admission/api"
    40  	"k8s.io/utils/ptr"
    41  
    42  	"github.com/onsi/ginkgo/v2"
    43  )
    44  
    45  var _ = SIGDescribe("CustomResourceFieldSelectors [Privileged:ClusterAdmin]", framework.WithFeatureGate(apiextensionsfeatures.CustomResourceFieldSelectors), func() {
    46  
    47  	f := framework.NewDefaultFramework("crd-selectable-fields")
    48  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    49  
    50  	ginkgo.Context("CustomResourceFieldSelectors", func() {
    51  		customResourceClient := func(crd *apiextensionsv1.CustomResourceDefinition, version string) (dynamic.NamespaceableResourceInterface, schema.GroupVersionResource) {
    52  			gvrs := fixtures.GetGroupVersionResourcesOfCustomResource(crd)
    53  			for _, gvr := range gvrs {
    54  				if gvr.Version == version {
    55  					return f.DynamicClient.Resource(gvr), gvr
    56  				}
    57  			}
    58  			ginkgo.Fail(fmt.Sprintf("Expected version '%s' in custom resource definition", version))
    59  			return nil, schema.GroupVersionResource{}
    60  		}
    61  
    62  		var apiVersions = []apiextensionsv1.CustomResourceDefinitionVersion{
    63  			{
    64  				Name:    "v1",
    65  				Served:  true,
    66  				Storage: true,
    67  				Schema: &apiextensionsv1.CustomResourceValidation{
    68  					OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
    69  						Type: "object",
    70  						Properties: map[string]apiextensionsv1.JSONSchemaProps{
    71  							"hostPort": {Type: "string"},
    72  						},
    73  					},
    74  				},
    75  				SelectableFields: []apiextensionsv1.SelectableField{
    76  					{JSONPath: ".hostPort"},
    77  				},
    78  			},
    79  			{
    80  				Name:    "v2",
    81  				Served:  true,
    82  				Storage: false,
    83  				Schema: &apiextensionsv1.CustomResourceValidation{
    84  					OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
    85  						Type: "object",
    86  						Properties: map[string]apiextensionsv1.JSONSchemaProps{
    87  							"host": {Type: "string"},
    88  							"port": {Type: "string"},
    89  						},
    90  					},
    91  				},
    92  				SelectableFields: []apiextensionsv1.SelectableField{
    93  					{JSONPath: ".host"},
    94  					{JSONPath: ".port"},
    95  				},
    96  			},
    97  		}
    98  
    99  		var certCtx *certContext
   100  		servicePort := int32(9443)
   101  		containerPort := int32(9444)
   102  
   103  		ginkgo.BeforeEach(func(ctx context.Context) {
   104  			ginkgo.DeferCleanup(cleanCRDWebhookTest, f.ClientSet, f.Namespace.Name)
   105  
   106  			ginkgo.By("Setting up server cert")
   107  			certCtx = setupServerCert(f.Namespace.Name, serviceCRDName)
   108  			createAuthReaderRoleBindingForCRDConversion(ctx, f, f.Namespace.Name)
   109  
   110  			deployCustomResourceWebhookAndService(ctx, f, imageutils.GetE2EImage(imageutils.Agnhost), certCtx, servicePort, containerPort)
   111  		})
   112  
   113  		/*
   114  			Release: v1.31
   115  			Testname: Custom Resource Definition, list and watch with selectable fields
   116  			Description: Create a Custom Resource Definition with SelectableFields. Create custom resources. Attempt to
   117  			list and watch custom resources with object selectors; the list and watch MUST return only custom resources
   118  			matching the field selector. Delete and update some of the custom resources. Attempt to list and watch the
   119  			custom resources with object selectors; the list and watch MUST return only the custom resources matching
   120  			the object selectors.
   121  		*/
   122  		framework.It("MUST list and watch custom resources matching the field selector", func(ctx context.Context) {
   123  			ginkgo.By("Creating a custom resource definition with selectable fields")
   124  			testcrd, err := crd.CreateMultiVersionTestCRD(f, "stable.example.com", func(crd *apiextensionsv1.CustomResourceDefinition) {
   125  				crd.Spec.Versions = apiVersions
   126  				crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
   127  					Strategy: apiextensionsv1.WebhookConverter,
   128  					Webhook: &apiextensionsv1.WebhookConversion{
   129  						ClientConfig: &apiextensionsv1.WebhookClientConfig{
   130  							CABundle: certCtx.signingCert,
   131  							Service: &apiextensionsv1.ServiceReference{
   132  								Namespace: f.Namespace.Name,
   133  								Name:      serviceCRDName,
   134  								Path:      ptr.To("/crdconvert"),
   135  								Port:      ptr.To(servicePort),
   136  							},
   137  						},
   138  						ConversionReviewVersions: []string{"v1", "v1beta1"},
   139  					},
   140  				}
   141  				crd.Spec.PreserveUnknownFields = false
   142  			})
   143  			if err != nil {
   144  				return
   145  			}
   146  			ginkgo.DeferCleanup(testcrd.CleanUp)
   147  
   148  			ginkgo.By("Creating a custom resource conversion webhook")
   149  			waitWebhookConversionReady(ctx, f, testcrd.Crd, testcrd.DynamicClients, "v2")
   150  			crd := testcrd.Crd
   151  
   152  			ginkgo.By("Watching with field selectors")
   153  
   154  			v2Client, gvr := customResourceClient(crd, "v2")
   155  			hostWatch, err := v2Client.Namespace(f.Namespace.Name).Watch(ctx, metav1.ListOptions{FieldSelector: "host=host1"})
   156  			framework.ExpectNoError(err, "watching custom resources with field selector")
   157  			v2hostPortWatch, err := v2Client.Namespace(f.Namespace.Name).Watch(ctx, metav1.ListOptions{FieldSelector: "host=host1,port=80"})
   158  			framework.ExpectNoError(err, "watching custom resources with field selector")
   159  
   160  			v1Client, _ := customResourceClient(crd, "v1")
   161  			v1hostPortWatch, err := v1Client.Namespace(f.Namespace.Name).Watch(ctx, metav1.ListOptions{FieldSelector: "hostPort=host1:80"})
   162  			framework.ExpectNoError(err, "watching custom resources with field selector")
   163  
   164  			ginkgo.By("Creating custom resources")
   165  			toCreate := []map[string]any{
   166  				{
   167  					"host": "host1",
   168  					"port": "80",
   169  				},
   170  				{
   171  					"host": "host1",
   172  					"port": "8080",
   173  				},
   174  				{
   175  					"host": "host2",
   176  				},
   177  			}
   178  
   179  			crNames := make([]string, len(toCreate))
   180  			for i, spec := range toCreate {
   181  				name := names.SimpleNameGenerator.GenerateName("selectable-field-cr")
   182  				crNames[i] = name
   183  
   184  				obj := map[string]interface{}{
   185  					"apiVersion": gvr.Group + "/" + gvr.Version,
   186  					"kind":       crd.Spec.Names.Kind,
   187  					"metadata": map[string]interface{}{
   188  						"name":      name,
   189  						"namespace": f.Namespace.Name,
   190  					},
   191  				}
   192  				for k, v := range spec {
   193  					obj[k] = v
   194  				}
   195  				_, err = v2Client.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: obj}, metav1.CreateOptions{})
   196  				framework.ExpectNoError(err, "creating custom resource")
   197  			}
   198  			ginkgo.By("Listing v2 custom resources with field selector host=host1")
   199  			list, err := v2Client.Namespace(f.Namespace.Name).List(ctx, metav1.ListOptions{FieldSelector: "host=host1"})
   200  			framework.ExpectNoError(err, "listing custom resources with field selector")
   201  			gomega.Expect(listResultToNames(list)).To(gomega.Equal(sets.New(crNames[0], crNames[1])))
   202  
   203  			ginkgo.By("Listing v2 custom resources with field selector host=host1,port=80")
   204  			list, err = v2Client.Namespace(f.Namespace.Name).List(ctx, metav1.ListOptions{FieldSelector: "host=host1,port=80"})
   205  			framework.ExpectNoError(err, "listing custom resources with field selector")
   206  			gomega.Expect(listResultToNames(list)).To(gomega.Equal(sets.New(crNames[0])))
   207  
   208  			ginkgo.By("Listing v1 custom resources with field selector hostPort=host1:80")
   209  			list, err = v1Client.Namespace(f.Namespace.Name).List(ctx, metav1.ListOptions{FieldSelector: "hostPort=host1:80"})
   210  			framework.ExpectNoError(err, "listing custom resources with field selector")
   211  			gomega.Expect(listResultToNames(list)).To(gomega.Equal(sets.New(crNames[0])))
   212  
   213  			ginkgo.By("Listing v1 custom resources with field selector hostPort=host1:8080")
   214  			list, err = v1Client.Namespace(f.Namespace.Name).List(ctx, metav1.ListOptions{FieldSelector: "hostPort=host1:8080"})
   215  			framework.ExpectNoError(err, "listing custom resources with field selector")
   216  			gomega.Expect(listResultToNames(list)).To(gomega.Equal(sets.New(crNames[1])))
   217  
   218  			ginkgo.By("Waiting for watch events to contain v2 custom resources for field selector host=host1")
   219  			gomega.Eventually(ctx, watchAccumulator(hostWatch)).WithPolling(5 * time.Millisecond).WithTimeout(30 * time.Second).
   220  				Should(gomega.Equal(addedEvents(sets.New(crNames[0], crNames[1]))))
   221  
   222  			ginkgo.By("Waiting for watch events to contain v2 custom resources for field selector host=host1,port=80")
   223  			gomega.Eventually(ctx, watchAccumulator(v2hostPortWatch)).WithPolling(5 * time.Millisecond).WithTimeout(30 * time.Second).
   224  				Should(gomega.Equal(addedEvents(sets.New(crNames[0]))))
   225  
   226  			ginkgo.By("Waiting for watch events to contain v1 custom resources for field selector hostPort=host1:80")
   227  			gomega.Eventually(ctx, watchAccumulator(v1hostPortWatch)).WithPolling(5 * time.Millisecond).WithTimeout(30 * time.Second).
   228  				Should(gomega.Equal(addedEvents(sets.New(crNames[0]))))
   229  
   230  			ginkgo.By("Deleting one custom resources to ensure that deletions are observed")
   231  			var gracePeriod int64 = 0
   232  			err = v2Client.Namespace(f.Namespace.Name).DeleteCollection(ctx, metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod}, metav1.ListOptions{FieldSelector: "host=host1,port=80"})
   233  			framework.ExpectNoError(err, "deleting custom resource")
   234  
   235  			ginkgo.By("Updating one custom resources to ensure that deletions are observed")
   236  			u, err := v2Client.Namespace(f.Namespace.Name).Get(ctx, crNames[1], metav1.GetOptions{})
   237  			framework.ExpectNoError(err, "getting custom resource")
   238  			u.Object["host"] = "host2"
   239  			_, err = v2Client.Namespace(f.Namespace.Name).Update(ctx, u, metav1.UpdateOptions{})
   240  			framework.ExpectNoError(err, "updating custom resource")
   241  
   242  			ginkgo.By("Listing v2 custom resources after updates and deletes for field selector host=host1")
   243  			list, err = v2Client.Namespace(f.Namespace.Name).List(ctx, metav1.ListOptions{FieldSelector: "host=host1"})
   244  			framework.ExpectNoError(err, "listing custom resources with field selector")
   245  			gomega.Expect(listResultToNames(list)).To(gomega.Equal(sets.New[string]()))
   246  
   247  			ginkgo.By("Listing v2 custom resources after updates and deletes for field selector host=host1,port=80")
   248  			list, err = v2Client.Namespace(f.Namespace.Name).List(ctx, metav1.ListOptions{FieldSelector: "host=host1,port=80"})
   249  			framework.ExpectNoError(err, "listing custom resources with field selector")
   250  			gomega.Expect(listResultToNames(list)).To(gomega.Equal(sets.New[string]()))
   251  
   252  			ginkgo.By("Waiting for v2 watch events after updates and deletes for field selector host=host1")
   253  			gomega.Eventually(ctx, watchAccumulator(hostWatch)).WithPolling(5 * time.Millisecond).WithTimeout(30 * time.Second).
   254  				Should(gomega.Equal(deletedEvents(sets.New(crNames[0], crNames[1]))))
   255  
   256  			ginkgo.By("Waiting for v2 watch events after updates and deletes for field selector host=host1,port=80")
   257  			gomega.Eventually(ctx, watchAccumulator(v2hostPortWatch)).WithPolling(5 * time.Millisecond).WithTimeout(30 * time.Second).
   258  				Should(gomega.Equal(deletedEvents(sets.New(crNames[0]))))
   259  
   260  			ginkgo.By("Waiting for v1 watch events after updates and deletes for field selector hostPort=host1:80")
   261  			gomega.Eventually(ctx, watchAccumulator(v1hostPortWatch)).WithPolling(5 * time.Millisecond).WithTimeout(30 * time.Second).
   262  				Should(gomega.Equal(deletedEvents(sets.New(crNames[0]))))
   263  		})
   264  
   265  	})
   266  })
   267  
   268  type accumulatedEvents struct {
   269  	added, deleted sets.Set[string]
   270  }
   271  
   272  func emptyEvents() *accumulatedEvents {
   273  	return &accumulatedEvents{added: sets.New[string](), deleted: sets.New[string]()}
   274  }
   275  
   276  func addedEvents(added sets.Set[string]) *accumulatedEvents {
   277  	return &accumulatedEvents{added: added, deleted: sets.New[string]()}
   278  }
   279  
   280  func deletedEvents(deleted sets.Set[string]) *accumulatedEvents {
   281  	return &accumulatedEvents{added: sets.New[string](), deleted: deleted}
   282  }
   283  
   284  func watchAccumulator(w watch.Interface) func(ctx context.Context) (*accumulatedEvents, error) {
   285  	result := emptyEvents()
   286  	return func(ctx context.Context) (*accumulatedEvents, error) {
   287  		for {
   288  			select {
   289  			case event := <-w.ResultChan():
   290  				obj, err := meta.Accessor(event.Object)
   291  				framework.ExpectNoError(err, "accessing object name")
   292  				switch event.Type {
   293  				case watch.Added:
   294  					result.added.Insert(obj.GetName())
   295  				case watch.Deleted:
   296  					result.deleted.Insert(obj.GetName())
   297  				}
   298  			default:
   299  				return result, nil
   300  			}
   301  		}
   302  	}
   303  }
   304  
   305  func listResultToNames(list *unstructured.UnstructuredList) sets.Set[string] {
   306  	found := sets.New[string]()
   307  	for _, i := range list.Items {
   308  		found.Insert(i.GetName())
   309  	}
   310  	return found
   311  }