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 }