...

Source file src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller_test.go

Documentation: k8s.io/apiextensions-apiserver/pkg/apiserver

     1  /*
     2  Copyright 2022 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 apiserver
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/stretchr/testify/require"
    25  	apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
    26  	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    28  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
    29  	"k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apiserver/pkg/endpoints/discovery"
    33  	"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
    34  )
    35  
    36  var coolFooCRD = &v1.CustomResourceDefinition{
    37  	TypeMeta: metav1.TypeMeta{
    38  		APIVersion: "apiextensions.k8s.io/v1",
    39  		Kind:       "CustomResourceDefinition",
    40  	},
    41  	ObjectMeta: metav1.ObjectMeta{
    42  		Name: "coolfoo.stable.example.com",
    43  	},
    44  	Spec: v1.CustomResourceDefinitionSpec{
    45  		Group: "stable.example.com",
    46  		Names: v1.CustomResourceDefinitionNames{
    47  			Plural:     "coolfoos",
    48  			Singular:   "coolfoo",
    49  			ShortNames: []string{"foo"},
    50  			Kind:       "CoolFoo",
    51  			ListKind:   "CoolFooList",
    52  			Categories: []string{"cool"},
    53  		},
    54  		Scope: v1.ClusterScoped,
    55  		Versions: []v1.CustomResourceDefinitionVersion{
    56  			{
    57  				Name:       "v1",
    58  				Served:     true,
    59  				Storage:    true,
    60  				Deprecated: false,
    61  				Subresources: &v1.CustomResourceSubresources{
    62  					// This CRD has a /status subresource
    63  					Status: &v1.CustomResourceSubresourceStatus{},
    64  				},
    65  				Schema: &v1.CustomResourceValidation{
    66  					// Unused by discovery
    67  					OpenAPIV3Schema: &v1.JSONSchemaProps{},
    68  				},
    69  			},
    70  		},
    71  		Conversion:            &v1.CustomResourceConversion{},
    72  		PreserveUnknownFields: false,
    73  	},
    74  	Status: v1.CustomResourceDefinitionStatus{
    75  		Conditions: []v1.CustomResourceDefinitionCondition{
    76  			{
    77  				Type:   v1.Established,
    78  				Status: v1.ConditionTrue,
    79  			},
    80  		},
    81  	},
    82  }
    83  
    84  var coolBarCRD = &v1.CustomResourceDefinition{
    85  	TypeMeta: metav1.TypeMeta{
    86  		APIVersion: "apiextensions.k8s.io/v1",
    87  		Kind:       "CustomResourceDefinition",
    88  	},
    89  	ObjectMeta: metav1.ObjectMeta{
    90  		Name: "coolbar.stable.example.com",
    91  	},
    92  	Spec: v1.CustomResourceDefinitionSpec{
    93  		Group: "stable.example.com",
    94  		Names: v1.CustomResourceDefinitionNames{
    95  			Plural:     "coolbars",
    96  			Singular:   "coolbar",
    97  			ShortNames: []string{"bar"},
    98  			Kind:       "CoolBar",
    99  			ListKind:   "CoolBarList",
   100  			Categories: []string{"cool"},
   101  		},
   102  		Scope: v1.ClusterScoped,
   103  		Versions: []v1.CustomResourceDefinitionVersion{
   104  			{
   105  				Name:       "v1",
   106  				Served:     true,
   107  				Storage:    true,
   108  				Deprecated: false,
   109  				Schema: &v1.CustomResourceValidation{
   110  					// Unused by discovery
   111  					OpenAPIV3Schema: &v1.JSONSchemaProps{},
   112  				},
   113  			},
   114  		},
   115  		Conversion:            &v1.CustomResourceConversion{},
   116  		PreserveUnknownFields: false,
   117  	},
   118  	Status: v1.CustomResourceDefinitionStatus{
   119  		Conditions: []v1.CustomResourceDefinitionCondition{
   120  			{
   121  				Type:   v1.Established,
   122  				Status: v1.ConditionTrue,
   123  			},
   124  		},
   125  	},
   126  }
   127  
   128  var coolFooDiscovery apidiscoveryv2.APIVersionDiscovery = apidiscoveryv2.APIVersionDiscovery{
   129  	Version:   "v1",
   130  	Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent,
   131  	Resources: []apidiscoveryv2.APIResourceDiscovery{
   132  		{
   133  			Resource:         "coolfoos",
   134  			Scope:            apidiscoveryv2.ScopeCluster,
   135  			SingularResource: "coolfoo",
   136  			Verbs:            []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"},
   137  			ShortNames:       []string{"foo"},
   138  			Categories:       []string{"cool"},
   139  			ResponseKind: &metav1.GroupVersionKind{
   140  				Group:   "stable.example.com",
   141  				Version: "v1",
   142  				Kind:    "CoolFoo",
   143  			},
   144  			Subresources: []apidiscoveryv2.APISubresourceDiscovery{
   145  				{
   146  					Subresource:   "status",
   147  					Verbs:         []string{"get", "patch", "update"},
   148  					AcceptedTypes: nil, // is this correct?
   149  					ResponseKind: &metav1.GroupVersionKind{
   150  						Group:   "stable.example.com",
   151  						Version: "v1",
   152  						Kind:    "CoolFoo",
   153  					},
   154  				},
   155  			},
   156  		},
   157  	},
   158  }
   159  
   160  var mergedDiscovery apidiscoveryv2.APIVersionDiscovery = apidiscoveryv2.APIVersionDiscovery{
   161  	Version:   "v1",
   162  	Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent,
   163  	Resources: []apidiscoveryv2.APIResourceDiscovery{
   164  		{
   165  			Resource:         "coolbars",
   166  			Scope:            apidiscoveryv2.ScopeCluster,
   167  			SingularResource: "coolbar",
   168  			Verbs:            []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"},
   169  			ShortNames:       []string{"bar"},
   170  			Categories:       []string{"cool"},
   171  			ResponseKind: &metav1.GroupVersionKind{
   172  				Group:   "stable.example.com",
   173  				Version: "v1",
   174  				Kind:    "CoolBar",
   175  			},
   176  		}, {
   177  			Resource:         "coolfoos",
   178  			Scope:            apidiscoveryv2.ScopeCluster,
   179  			SingularResource: "coolfoo",
   180  			Verbs:            []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"},
   181  			ShortNames:       []string{"foo"},
   182  			Categories:       []string{"cool"},
   183  			ResponseKind: &metav1.GroupVersionKind{
   184  				Group:   "stable.example.com",
   185  				Version: "v1",
   186  				Kind:    "CoolFoo",
   187  			},
   188  			Subresources: []apidiscoveryv2.APISubresourceDiscovery{
   189  				{
   190  					Subresource:   "status",
   191  					Verbs:         []string{"get", "patch", "update"},
   192  					AcceptedTypes: nil, // is this correct?
   193  					ResponseKind: &metav1.GroupVersionKind{
   194  						Group:   "stable.example.com",
   195  						Version: "v1",
   196  						Kind:    "CoolFoo",
   197  					},
   198  				},
   199  			},
   200  		},
   201  	},
   202  }
   203  
   204  func init() {
   205  	// Not testing against an apiserver, so just assume names are accepted
   206  	coolFooCRD.Status.AcceptedNames = coolFooCRD.Spec.Names
   207  	coolBarCRD.Status.AcceptedNames = coolBarCRD.Spec.Names
   208  }
   209  
   210  // Provides an apiextensions-apiserver client
   211  type testEnvironment struct {
   212  	clientset.Interface
   213  
   214  	// Discovery test details
   215  	versionDiscoveryHandler
   216  	groupDiscoveryHandler
   217  
   218  	aggregated.FakeResourceManager
   219  }
   220  
   221  func (env *testEnvironment) Start(ctx context.Context) {
   222  	discoverySyncedCh := make(chan struct{})
   223  
   224  	factory := externalversions.NewSharedInformerFactoryWithOptions(
   225  		env.Interface, 30*time.Second)
   226  
   227  	discoveryController := NewDiscoveryController(
   228  		factory.Apiextensions().V1().CustomResourceDefinitions(),
   229  		&env.versionDiscoveryHandler,
   230  		&env.groupDiscoveryHandler,
   231  		env.FakeResourceManager,
   232  	)
   233  
   234  	factory.Start(ctx.Done())
   235  	go discoveryController.Run(ctx.Done(), discoverySyncedCh)
   236  
   237  	select {
   238  	case <-discoverySyncedCh:
   239  	case <-ctx.Done():
   240  	}
   241  }
   242  
   243  func setup() *testEnvironment {
   244  	env := &testEnvironment{
   245  		Interface:           fake.NewSimpleClientset(),
   246  		FakeResourceManager: aggregated.NewFakeResourceManager(),
   247  		versionDiscoveryHandler: versionDiscoveryHandler{
   248  			discovery: make(map[schema.GroupVersion]*discovery.APIVersionHandler),
   249  		},
   250  		groupDiscoveryHandler: groupDiscoveryHandler{
   251  			discovery: make(map[string]*discovery.APIGroupHandler),
   252  		},
   253  	}
   254  
   255  	return env
   256  }
   257  
   258  func TestResourceManagerExistingCRD(t *testing.T) {
   259  	ctx, cancel := context.WithCancel(context.Background())
   260  	defer cancel()
   261  
   262  	env := setup()
   263  	_, err := env.Interface.
   264  		ApiextensionsV1().
   265  		CustomResourceDefinitions().
   266  		Create(
   267  			ctx,
   268  			coolFooCRD,
   269  			metav1.CreateOptions{
   270  				FieldManager: "resource-manager-test",
   271  			},
   272  		)
   273  
   274  	require.NoError(t, err)
   275  
   276  	env.FakeResourceManager.Expect().
   277  		AddGroupVersion(coolFooCRD.Spec.Group, coolFooDiscovery)
   278  	for _, v := range coolFooCRD.Spec.Versions {
   279  		env.FakeResourceManager.Expect().
   280  			SetGroupVersionPriority(metav1.GroupVersion{Group: coolFooCRD.Spec.Group, Version: v.Name}, 1000, 100)
   281  	}
   282  
   283  	env.FakeResourceManager.Expect().
   284  		AddGroupVersion(coolFooCRD.Spec.Group, coolFooDiscovery)
   285  	for _, v := range coolFooCRD.Spec.Versions {
   286  		env.FakeResourceManager.Expect().
   287  			SetGroupVersionPriority(metav1.GroupVersion{Group: coolFooCRD.Spec.Group, Version: v.Name}, 1000, 100)
   288  	}
   289  
   290  	env.Start(ctx)
   291  	err = env.FakeResourceManager.WaitForActions(ctx, 1*time.Second)
   292  	require.NoError(t, err)
   293  }
   294  
   295  // Tests that if a CRD is added a runtime, the discovery controller will
   296  // put its information in the discovery document
   297  func TestResourceManagerAddedCRD(t *testing.T) {
   298  	ctx, cancel := context.WithCancel(context.Background())
   299  	defer cancel()
   300  
   301  	env := setup()
   302  	env.FakeResourceManager.Expect().
   303  		AddGroupVersion(coolFooCRD.Spec.Group, coolFooDiscovery)
   304  	for _, v := range coolFooCRD.Spec.Versions {
   305  		env.FakeResourceManager.Expect().
   306  			SetGroupVersionPriority(metav1.GroupVersion{Group: coolFooCRD.Spec.Group, Version: v.Name}, 1000, 100)
   307  	}
   308  
   309  	env.Start(ctx)
   310  
   311  	// Create CRD after the controller has already started
   312  	_, err := env.Interface.
   313  		ApiextensionsV1().
   314  		CustomResourceDefinitions().
   315  		Create(
   316  			ctx,
   317  			coolFooCRD,
   318  			metav1.CreateOptions{
   319  				FieldManager: "resource-manager-test",
   320  			},
   321  		)
   322  
   323  	require.NoError(t, err)
   324  
   325  	err = env.FakeResourceManager.WaitForActions(ctx, 1*time.Second)
   326  	require.NoError(t, err)
   327  }
   328  
   329  // Test that having multiple CRDs in the same version will add both
   330  // versions to discovery.
   331  func TestMultipleCRDSameVersion(t *testing.T) {
   332  	ctx, cancel := context.WithCancel(context.Background())
   333  	defer cancel()
   334  
   335  	env := setup()
   336  	env.Start(ctx)
   337  
   338  	_, err := env.Interface.
   339  		ApiextensionsV1().
   340  		CustomResourceDefinitions().
   341  		Create(
   342  			ctx,
   343  			coolFooCRD,
   344  			metav1.CreateOptions{
   345  				FieldManager: "resource-manager-test",
   346  			},
   347  		)
   348  
   349  	require.NoError(t, err)
   350  	env.FakeResourceManager.Expect().
   351  		AddGroupVersion(coolFooCRD.Spec.Group, coolFooDiscovery)
   352  	for _, versionEntry := range coolFooCRD.Spec.Versions {
   353  		env.FakeResourceManager.Expect().SetGroupVersionPriority(metav1.GroupVersion{Group: coolFooCRD.Spec.Group, Version: versionEntry.Name}, 1000, 100)
   354  	}
   355  	err = env.FakeResourceManager.WaitForActions(ctx, 1*time.Second)
   356  	require.NoError(t, err)
   357  
   358  	_, err = env.Interface.
   359  		ApiextensionsV1().
   360  		CustomResourceDefinitions().
   361  		Create(
   362  			ctx,
   363  			coolBarCRD,
   364  			metav1.CreateOptions{
   365  				FieldManager: "resource-manager-test",
   366  			},
   367  		)
   368  	require.NoError(t, err)
   369  
   370  	env.FakeResourceManager.Expect().
   371  		AddGroupVersion(coolFooCRD.Spec.Group, mergedDiscovery)
   372  	for _, versionEntry := range coolFooCRD.Spec.Versions {
   373  		env.FakeResourceManager.Expect().SetGroupVersionPriority(metav1.GroupVersion{Group: coolFooCRD.Spec.Group, Version: versionEntry.Name}, 1000, 100)
   374  	}
   375  	err = env.FakeResourceManager.WaitForActions(ctx, 1*time.Second)
   376  	require.NoError(t, err)
   377  }
   378  
   379  // Tests that if a CRD is deleted at runtime, the discovery controller will
   380  // remove its information from its ResourceManager
   381  func TestDiscoveryControllerResourceManagerRemovedCRD(t *testing.T) {
   382  	ctx, cancel := context.WithCancel(context.Background())
   383  	defer cancel()
   384  
   385  	env := setup()
   386  	env.Start(ctx)
   387  
   388  	// Create CRD after the controller has already started
   389  	_, err := env.Interface.
   390  		ApiextensionsV1().
   391  		CustomResourceDefinitions().
   392  		Create(
   393  			ctx,
   394  			coolFooCRD,
   395  			metav1.CreateOptions{},
   396  		)
   397  
   398  	require.NoError(t, err)
   399  
   400  	// Wait for the Controller to pick up the Create event and add it to the
   401  	// Resource Manager
   402  	env.FakeResourceManager.Expect().
   403  		AddGroupVersion(coolFooCRD.Spec.Group, coolFooDiscovery)
   404  	for _, versionEntry := range coolFooCRD.Spec.Versions {
   405  		env.FakeResourceManager.Expect().SetGroupVersionPriority(metav1.GroupVersion{Group: coolFooCRD.Spec.Group, Version: versionEntry.Name}, 1000, 100)
   406  	}
   407  	err = env.FakeResourceManager.WaitForActions(ctx, 1*time.Second)
   408  	require.NoError(t, err)
   409  
   410  	err = env.Interface.
   411  		ApiextensionsV1().
   412  		CustomResourceDefinitions().
   413  		Delete(ctx, coolFooCRD.Name, metav1.DeleteOptions{})
   414  
   415  	require.NoError(t, err)
   416  
   417  	// Wait for the Controller to detect there are no more CRDs of this group
   418  	// and remove the entire group
   419  	env.FakeResourceManager.Expect().RemoveGroup(coolFooCRD.Spec.Group)
   420  
   421  	err = env.FakeResourceManager.WaitForActions(ctx, 1*time.Second)
   422  	require.NoError(t, err)
   423  }
   424  

View as plain text