123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- /*
- Copyright 2019 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package etcd
- import (
- "context"
- "encoding/json"
- "strings"
- "testing"
- "time"
- apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
- crdclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
- "k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/apimachinery/pkg/util/sets"
- "k8s.io/apimachinery/pkg/util/wait"
- "k8s.io/client-go/dynamic"
- apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
- apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
- )
- // TestOverlappingBuiltInResources ensures the list of group-resources the custom resource finalizer should skip is up to date
- func TestOverlappingBuiltInResources(t *testing.T) {
- // Verify built-in resources that overlap with computed CRD storage paths are listed in OverlappingBuiltInResources()
- detectedOverlappingResources := map[schema.GroupResource]bool{}
- for gvr, gvrData := range GetEtcdStorageData() {
- if !strings.HasSuffix(gvr.Group, ".k8s.io") {
- // only fully-qualified group names can exist as CRDs
- continue
- }
- if !strings.Contains(gvrData.ExpectedEtcdPath, "/"+gvr.Group+"/"+gvr.Resource+"/") {
- // CRDs persist in storage under .../<group>/<resource>/...
- continue
- }
- detectedOverlappingResources[gvr.GroupResource()] = true
- }
- for detected := range detectedOverlappingResources {
- if !finalizer.OverlappingBuiltInResources()[detected] {
- t.Errorf("built-in resource %#v would overlap with custom resource storage if a CRD was created for the same group/resource", detected)
- t.Errorf("add %#v to the OverlappingBuiltInResources() list to prevent deletion by the CRD finalizer", detected)
- }
- }
- for skip := range finalizer.OverlappingBuiltInResources() {
- if !detectedOverlappingResources[skip] {
- t.Errorf("resource %#v does not overlap with any built-in resources in storage, but is skipped for CRD finalization by OverlappingBuiltInResources()", skip)
- t.Errorf("remove %#v from OverlappingBuiltInResources() to ensure CRD finalization cleans up stored custom resources", skip)
- }
- }
- }
- // TestOverlappingCustomResourceAPIService ensures creating and deleting a custom resource overlapping with APIServices does not destroy APIService data
- func TestOverlappingCustomResourceAPIService(t *testing.T) {
- master := StartRealMasterOrDie(t)
- defer master.Cleanup()
- apiServiceClient, err := apiregistrationclient.NewForConfig(master.Config)
- if err != nil {
- t.Fatal(err)
- }
- crdClient, err := crdclient.NewForConfig(master.Config)
- if err != nil {
- t.Fatal(err)
- }
- dynamicClient, err := dynamic.NewForConfig(master.Config)
- if err != nil {
- t.Fatal(err)
- }
- // Verify APIServices can be listed
- apiServices, err := apiServiceClient.APIServices().List(context.TODO(), metav1.ListOptions{})
- if err != nil {
- t.Fatal(err)
- }
- apiServiceNames := sets.NewString()
- for _, s := range apiServices.Items {
- apiServiceNames.Insert(s.Name)
- }
- if len(apiServices.Items) == 0 {
- t.Fatal("expected APIService objects, got none")
- }
- // Create a CRD defining an overlapping apiregistration.k8s.io apiservices resource with an incompatible schema
- crdCRD, err := crdClient.CustomResourceDefinitions().Create(context.TODO(), &apiextensionsv1.CustomResourceDefinition{
- ObjectMeta: metav1.ObjectMeta{
- Name: "apiservices.apiregistration.k8s.io",
- Annotations: map[string]string{"api-approved.kubernetes.io": "unapproved, testing only"},
- },
- Spec: apiextensionsv1.CustomResourceDefinitionSpec{
- Group: "apiregistration.k8s.io",
- Scope: apiextensionsv1.ClusterScoped,
- Names: apiextensionsv1.CustomResourceDefinitionNames{Plural: "apiservices", Singular: "customapiservice", Kind: "CustomAPIService", ListKind: "CustomAPIServiceList"},
- Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
- {
- Name: "v1",
- Served: true,
- Storage: true,
- Schema: &apiextensionsv1.CustomResourceValidation{
- OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
- Type: "object",
- Required: []string{"foo"},
- Properties: map[string]apiextensionsv1.JSONSchemaProps{
- "foo": {Type: "string"},
- "bar": {Type: "string", Default: &apiextensionsv1.JSON{Raw: []byte(`"default"`)}},
- },
- },
- },
- },
- },
- },
- }, metav1.CreateOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // Wait until it is established
- if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
- crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
- if err != nil {
- return false, err
- }
- for _, condition := range crd.Status.Conditions {
- if condition.Status == apiextensionsv1.ConditionTrue && condition.Type == apiextensionsv1.Established {
- return true, nil
- }
- }
- conditionJSON, _ := json.Marshal(crd.Status.Conditions)
- t.Logf("waiting for establishment (conditions: %s)", string(conditionJSON))
- return false, nil
- }); err != nil {
- t.Fatal(err)
- }
- // Make sure API requests are still handled by the built-in handler (and return built-in kinds)
- // Listing v1 succeeds
- v1DynamicList, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apiregistration.k8s.io", Version: "v1", Resource: "apiservices"}).List(metav1.ListOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // Result was served by built-in handler, not CR handler
- if _, hasDefaultedCRField := v1DynamicList.Items[0].Object["spec"].(map[string]interface{})["bar"]; hasDefaultedCRField {
- t.Fatalf("expected no CR defaulting, got %#v", v1DynamicList.Items[0].Object)
- }
- // Creating v1 succeeds (built-in validation, not CR validation)
- testAPIService, err := apiServiceClient.APIServices().Create(context.TODO(), &apiregistrationv1.APIService{
- ObjectMeta: metav1.ObjectMeta{Name: "v1.example.com"},
- Spec: apiregistrationv1.APIServiceSpec{
- Group: "example.com",
- Version: "v1",
- VersionPriority: 100,
- GroupPriorityMinimum: 100,
- },
- }, metav1.CreateOptions{})
- if err != nil {
- t.Fatal(err)
- }
- err = apiServiceClient.APIServices().Delete(context.TODO(), testAPIService.Name, &metav1.DeleteOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // discovery is handled by the built-in handler
- v1Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiregistration.k8s.io/v1")
- if err != nil {
- t.Fatal(err)
- }
- for _, r := range v1Resources.APIResources {
- if r.Name == "apiservices" {
- if r.Kind != "APIService" {
- t.Errorf("expected kind=APIService in discovery, got %s", r.Kind)
- }
- }
- }
- v2Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiregistration.k8s.io/v2")
- if err == nil {
- t.Fatalf("expected error looking up apiregistration.k8s.io/v2 discovery, got %#v", v2Resources)
- }
- // Delete the overlapping CRD
- err = crdClient.CustomResourceDefinitions().Delete(context.TODO(), crdCRD.Name, &metav1.DeleteOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // Make sure the CRD deletion succeeds
- if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
- crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
- if apierrors.IsNotFound(err) {
- return true, nil
- }
- if err != nil {
- return false, err
- }
- conditionJSON, _ := json.Marshal(crd.Status.Conditions)
- t.Logf("waiting for deletion (conditions: %s)", string(conditionJSON))
- return false, nil
- }); err != nil {
- t.Fatal(err)
- }
- // Make sure APIService objects are not removed
- time.Sleep(5 * time.Second)
- finalAPIServices, err := apiServiceClient.APIServices().List(context.TODO(), metav1.ListOptions{})
- if err != nil {
- t.Fatal(err)
- }
- if len(finalAPIServices.Items) != len(apiServices.Items) {
- t.Fatalf("expected %d APIService objects, got %d", len(apiServices.Items), len(finalAPIServices.Items))
- }
- }
- // TestOverlappingCustomResourceCustomResourceDefinition ensures creating and deleting a custom resource overlapping with CustomResourceDefinition does not destroy CustomResourceDefinition data
- func TestOverlappingCustomResourceCustomResourceDefinition(t *testing.T) {
- master := StartRealMasterOrDie(t)
- defer master.Cleanup()
- crdClient, err := crdclient.NewForConfig(master.Config)
- if err != nil {
- t.Fatal(err)
- }
- dynamicClient, err := dynamic.NewForConfig(master.Config)
- if err != nil {
- t.Fatal(err)
- }
- // Verify CustomResourceDefinitions can be listed
- crds, err := crdClient.CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{})
- if err != nil {
- t.Fatal(err)
- }
- crdNames := sets.NewString()
- for _, s := range crds.Items {
- crdNames.Insert(s.Name)
- }
- if len(crds.Items) == 0 {
- t.Fatal("expected CustomResourceDefinition objects, got none")
- }
- // Create a CRD defining an overlapping apiregistration.k8s.io apiservices resource with an incompatible schema
- crdCRD, err := crdClient.CustomResourceDefinitions().Create(context.TODO(), &apiextensionsv1.CustomResourceDefinition{
- ObjectMeta: metav1.ObjectMeta{
- Name: "customresourcedefinitions.apiextensions.k8s.io",
- Annotations: map[string]string{"api-approved.kubernetes.io": "unapproved, testing only"},
- },
- Spec: apiextensionsv1.CustomResourceDefinitionSpec{
- Group: "apiextensions.k8s.io",
- Scope: apiextensionsv1.ClusterScoped,
- Names: apiextensionsv1.CustomResourceDefinitionNames{
- Plural: "customresourcedefinitions",
- Singular: "customcustomresourcedefinition",
- Kind: "CustomCustomResourceDefinition",
- ListKind: "CustomAPIServiceList",
- },
- Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
- {
- Name: "v1",
- Served: true,
- Storage: true,
- Schema: &apiextensionsv1.CustomResourceValidation{
- OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
- Type: "object",
- Required: []string{"foo"},
- Properties: map[string]apiextensionsv1.JSONSchemaProps{
- "foo": {Type: "string"},
- "bar": {Type: "string", Default: &apiextensionsv1.JSON{Raw: []byte(`"default"`)}},
- },
- },
- },
- },
- },
- },
- }, metav1.CreateOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // Wait until it is established
- if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
- crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
- if err != nil {
- return false, err
- }
- for _, condition := range crd.Status.Conditions {
- if condition.Status == apiextensionsv1.ConditionTrue && condition.Type == apiextensionsv1.Established {
- return true, nil
- }
- }
- conditionJSON, _ := json.Marshal(crd.Status.Conditions)
- t.Logf("waiting for establishment (conditions: %s)", string(conditionJSON))
- return false, nil
- }); err != nil {
- t.Fatal(err)
- }
- // Make sure API requests are still handled by the built-in handler (and return built-in kinds)
- // Listing v1 succeeds
- v1DynamicList, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}).List(metav1.ListOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // Result was served by built-in handler, not CR handler
- if _, hasDefaultedCRField := v1DynamicList.Items[0].Object["spec"].(map[string]interface{})["bar"]; hasDefaultedCRField {
- t.Fatalf("expected no CR defaulting, got %#v", v1DynamicList.Items[0].Object)
- }
- // Updating v1 succeeds (built-in validation, not CR validation)
- _, err = crdClient.CustomResourceDefinitions().Patch(context.TODO(), crdCRD.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"updated"}}}`), metav1.PatchOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // discovery is handled by the built-in handler
- v1Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiextensions.k8s.io/v1")
- if err != nil {
- t.Fatal(err)
- }
- for _, r := range v1Resources.APIResources {
- if r.Name == "customresourcedefinitions" {
- if r.Kind != "CustomResourceDefinition" {
- t.Errorf("expected kind=CustomResourceDefinition in discovery, got %s", r.Kind)
- }
- }
- }
- v2Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiextensions.k8s.io/v2")
- if err == nil {
- t.Fatalf("expected error looking up apiregistration.k8s.io/v2 discovery, got %#v", v2Resources)
- }
- // Delete the overlapping CRD
- err = crdClient.CustomResourceDefinitions().Delete(context.TODO(), crdCRD.Name, &metav1.DeleteOptions{})
- if err != nil {
- t.Fatal(err)
- }
- // Make sure the CRD deletion succeeds
- if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
- crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
- if apierrors.IsNotFound(err) {
- return true, nil
- }
- if err != nil {
- return false, err
- }
- conditionJSON, _ := json.Marshal(crd.Status.Conditions)
- t.Logf("waiting for deletion (conditions: %s)", string(conditionJSON))
- return false, nil
- }); err != nil {
- t.Fatal(err)
- }
- // Make sure other CustomResourceDefinition objects are not removed
- time.Sleep(5 * time.Second)
- finalCRDs, err := crdClient.CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{})
- if err != nil {
- t.Fatal(err)
- }
- if len(finalCRDs.Items) != len(crds.Items) {
- t.Fatalf("expected %d APIService objects, got %d", len(crds.Items), len(finalCRDs.Items))
- }
- }
|