...

Source file src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/controller_test.go

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

     1  /*
     2  Copyright 2023 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 openapi
    18  
    19  import (
    20  	"context"
    21  	"io"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"testing"
    25  	"time"
    26  
    27  	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    28  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    29  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
    30  	"k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
    31  	"k8s.io/kube-openapi/pkg/handler"
    32  
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/util/wait"
    35  
    36  	"k8s.io/kube-openapi/pkg/validation/spec"
    37  )
    38  
    39  func TestBasicAddRemove(t *testing.T) {
    40  	env, ctx := setup(t)
    41  	env.runFunc()
    42  	defer env.cleanFunc()
    43  
    44  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
    45  	env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
    46  	s := env.fetchOpenAPIOrDie()
    47  	env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
    48  	env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
    49  
    50  	t.Logf("Removing CRD %s", coolFooCRD.Name)
    51  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Delete(ctx, coolFooCRD.Name, metav1.DeleteOptions{})
    52  	env.pollForPathNotExists("/apis/stable.example.com/v1/coolfoos")
    53  	s = env.fetchOpenAPIOrDie()
    54  	env.expectNoPath(s, "/apis/stable.example.com/v1/coolfoos")
    55  }
    56  
    57  func TestTwoCRDsSameGroup(t *testing.T) {
    58  	env, ctx := setup(t)
    59  	env.runFunc()
    60  	defer env.cleanFunc()
    61  
    62  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
    63  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolBarCRD, metav1.CreateOptions{})
    64  	env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
    65  	env.pollForPathExists("/apis/stable.example.com/v1/coolbars")
    66  	s := env.fetchOpenAPIOrDie()
    67  	env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
    68  	env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
    69  	env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
    70  }
    71  
    72  func TestCRDMultiVersion(t *testing.T) {
    73  	env, ctx := setup(t)
    74  	env.runFunc()
    75  	defer env.cleanFunc()
    76  
    77  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolMultiVersion, metav1.CreateOptions{})
    78  	env.pollForPathExists("/apis/stable.example.com/v1/coolbars")
    79  	env.pollForPathExists("/apis/stable.example.com/v1beta1/coolbars")
    80  	s := env.fetchOpenAPIOrDie()
    81  	env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
    82  	env.expectPath(s, "/apis/stable.example.com/v1beta1/coolbars")
    83  	env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
    84  }
    85  
    86  func TestCRDMultiVersionUpdate(t *testing.T) {
    87  	env, ctx := setup(t)
    88  	env.runFunc()
    89  	defer env.cleanFunc()
    90  
    91  	crd, _ := env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolMultiVersion, metav1.CreateOptions{})
    92  	env.pollForPathExists("/apis/stable.example.com/v1/coolbars")
    93  	env.pollForPathExists("/apis/stable.example.com/v1beta1/coolbars")
    94  	s := env.fetchOpenAPIOrDie()
    95  	env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
    96  	env.expectPath(s, "/apis/stable.example.com/v1beta1/coolbars")
    97  	env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
    98  
    99  	t.Log("Removing version v1beta1")
   100  	crd.Spec.Versions = crd.Spec.Versions[1:]
   101  	crd.Generation += 1
   102  	// Generation is updated before storage to etcd. Since we don't have that in the fake client, manually increase it.
   103  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, crd, metav1.UpdateOptions{})
   104  	env.pollForPathNotExists("/apis/stable.example.com/v1beta1/coolbars")
   105  	s = env.fetchOpenAPIOrDie()
   106  	env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
   107  	env.expectNoPath(s, "/apis/stable.example.com/v1beta1/coolbars")
   108  	env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
   109  }
   110  
   111  func TestExistingCRDBeforeAPIServerStart(t *testing.T) {
   112  	env, ctx := setup(t)
   113  	defer env.cleanFunc()
   114  
   115  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
   116  	env.runFunc()
   117  	env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
   118  	s := env.fetchOpenAPIOrDie()
   119  
   120  	env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
   121  	env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
   122  }
   123  
   124  func TestUpdate(t *testing.T) {
   125  	env, ctx := setup(t)
   126  	env.runFunc()
   127  	defer env.cleanFunc()
   128  
   129  	crd, _ := env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
   130  	env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
   131  	s := env.fetchOpenAPIOrDie()
   132  	env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
   133  	env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
   134  
   135  	t.Log("Updating CRD CoolFoo")
   136  	crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["num"] = v1.JSONSchemaProps{Type: "integer", Description: "updated description"}
   137  	crd.Generation += 1
   138  	// Generation is updated before storage to etcd. Since we don't have that in the fake client, manually increase it.
   139  
   140  	env.Interface.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, crd, metav1.UpdateOptions{})
   141  	env.pollForCondition(func(s *spec.Swagger) bool {
   142  		return s.Definitions["com.example.stable.v1.CoolFoo"].Properties["num"].Description == crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["num"].Description
   143  	})
   144  	s = env.fetchOpenAPIOrDie()
   145  
   146  	// Ensure that description is updated
   147  	if s.Definitions["com.example.stable.v1.CoolFoo"].Properties["num"].Description != crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["num"].Description {
   148  		t.Error("Error: Description not updated")
   149  	}
   150  	env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
   151  }
   152  
   153  var coolFooCRD = &v1.CustomResourceDefinition{
   154  	TypeMeta: metav1.TypeMeta{
   155  		APIVersion: "apiextensions.k8s.io/v1",
   156  		Kind:       "CustomResourceDefinition",
   157  	},
   158  	ObjectMeta: metav1.ObjectMeta{
   159  		Name: "coolfoo.stable.example.com",
   160  	},
   161  	Spec: v1.CustomResourceDefinitionSpec{
   162  		Group: "stable.example.com",
   163  		Names: v1.CustomResourceDefinitionNames{
   164  			Plural:     "coolfoos",
   165  			Singular:   "coolfoo",
   166  			ShortNames: []string{"foo"},
   167  			Kind:       "CoolFoo",
   168  			ListKind:   "CoolFooList",
   169  		},
   170  		Scope: v1.ClusterScoped,
   171  		Versions: []v1.CustomResourceDefinitionVersion{
   172  			{
   173  				Name:       "v1",
   174  				Served:     true,
   175  				Storage:    true,
   176  				Deprecated: false,
   177  				Subresources: &v1.CustomResourceSubresources{
   178  					// This CRD has a /status subresource
   179  					Status: &v1.CustomResourceSubresourceStatus{},
   180  				},
   181  				Schema: &v1.CustomResourceValidation{
   182  					OpenAPIV3Schema: &v1.JSONSchemaProps{
   183  						Type:       "object",
   184  						Properties: map[string]v1.JSONSchemaProps{"num": {Type: "integer", Description: "description"}},
   185  					},
   186  				},
   187  			},
   188  		},
   189  		Conversion: &v1.CustomResourceConversion{},
   190  	},
   191  	Status: v1.CustomResourceDefinitionStatus{
   192  		Conditions: []v1.CustomResourceDefinitionCondition{
   193  			{
   194  				Type:   v1.Established,
   195  				Status: v1.ConditionTrue,
   196  			},
   197  		},
   198  	},
   199  }
   200  
   201  var coolBarCRD = &v1.CustomResourceDefinition{
   202  	TypeMeta: metav1.TypeMeta{
   203  		APIVersion: "apiextensions.k8s.io/v1",
   204  		Kind:       "CustomResourceDefinition",
   205  	},
   206  	ObjectMeta: metav1.ObjectMeta{
   207  		Name: "coolbar.stable.example.com",
   208  	},
   209  	Spec: v1.CustomResourceDefinitionSpec{
   210  		Group: "stable.example.com",
   211  		Names: v1.CustomResourceDefinitionNames{
   212  			Plural:     "coolbars",
   213  			Singular:   "coolbar",
   214  			ShortNames: []string{"bar"},
   215  			Kind:       "CoolBar",
   216  			ListKind:   "CoolBarList",
   217  		},
   218  		Scope: v1.ClusterScoped,
   219  		Versions: []v1.CustomResourceDefinitionVersion{
   220  			{
   221  				Name:       "v1",
   222  				Served:     true,
   223  				Storage:    true,
   224  				Deprecated: false,
   225  				Subresources: &v1.CustomResourceSubresources{
   226  					// This CRD has a /status subresource
   227  					Status: &v1.CustomResourceSubresourceStatus{},
   228  				},
   229  				Schema: &v1.CustomResourceValidation{
   230  					OpenAPIV3Schema: &v1.JSONSchemaProps{
   231  						Type:       "object",
   232  						Properties: map[string]v1.JSONSchemaProps{"num": {Type: "integer", Description: "description"}},
   233  					},
   234  				},
   235  			},
   236  		},
   237  		Conversion: &v1.CustomResourceConversion{},
   238  	},
   239  	Status: v1.CustomResourceDefinitionStatus{
   240  		Conditions: []v1.CustomResourceDefinitionCondition{
   241  			{
   242  				Type:   v1.Established,
   243  				Status: v1.ConditionTrue,
   244  			},
   245  		},
   246  	},
   247  }
   248  
   249  var coolMultiVersion = &v1.CustomResourceDefinition{
   250  	TypeMeta: metav1.TypeMeta{
   251  		APIVersion: "apiextensions.k8s.io/v1",
   252  		Kind:       "CustomResourceDefinition",
   253  	},
   254  	ObjectMeta: metav1.ObjectMeta{
   255  		Name: "coolbar.stable.example.com",
   256  	},
   257  	Spec: v1.CustomResourceDefinitionSpec{
   258  		Group: "stable.example.com",
   259  		Names: v1.CustomResourceDefinitionNames{
   260  			Plural:     "coolbars",
   261  			Singular:   "coolbar",
   262  			ShortNames: []string{"bar"},
   263  			Kind:       "CoolBar",
   264  			ListKind:   "CoolBarList",
   265  		},
   266  		Scope: v1.ClusterScoped,
   267  		Versions: []v1.CustomResourceDefinitionVersion{
   268  			{
   269  				Name:       "v1beta1",
   270  				Served:     true,
   271  				Storage:    true,
   272  				Deprecated: false,
   273  				Subresources: &v1.CustomResourceSubresources{
   274  					// This CRD has a /status subresource
   275  					Status: &v1.CustomResourceSubresourceStatus{},
   276  				},
   277  				Schema: &v1.CustomResourceValidation{
   278  					OpenAPIV3Schema: &v1.JSONSchemaProps{
   279  						Type:       "object",
   280  						Properties: map[string]v1.JSONSchemaProps{"num": {Type: "integer", Description: "description"}},
   281  					},
   282  				},
   283  			},
   284  
   285  			{
   286  				Name:       "v1",
   287  				Served:     true,
   288  				Storage:    true,
   289  				Deprecated: false,
   290  				Subresources: &v1.CustomResourceSubresources{
   291  					// This CRD has a /status subresource
   292  					Status: &v1.CustomResourceSubresourceStatus{},
   293  				},
   294  				Schema: &v1.CustomResourceValidation{
   295  					OpenAPIV3Schema: &v1.JSONSchemaProps{
   296  						Type:       "object",
   297  						Properties: map[string]v1.JSONSchemaProps{"test": {Type: "integer", Description: "foo"}},
   298  					},
   299  				},
   300  			},
   301  		},
   302  		Conversion: &v1.CustomResourceConversion{},
   303  	},
   304  	Status: v1.CustomResourceDefinitionStatus{
   305  		Conditions: []v1.CustomResourceDefinitionCondition{
   306  			{
   307  				Type:   v1.Established,
   308  				Status: v1.ConditionTrue,
   309  			},
   310  		},
   311  	},
   312  }
   313  
   314  type testEnv struct {
   315  	t *testing.T
   316  	clientset.Interface
   317  	mux       *http.ServeMux
   318  	cleanFunc func()
   319  	runFunc   func()
   320  }
   321  
   322  func setup(t *testing.T) (*testEnv, context.Context) {
   323  	env := &testEnv{
   324  		Interface: fake.NewSimpleClientset(),
   325  		t:         t,
   326  	}
   327  
   328  	factory := externalversions.NewSharedInformerFactoryWithOptions(
   329  		env.Interface, 30*time.Second)
   330  
   331  	c := NewController(factory.Apiextensions().V1().CustomResourceDefinitions())
   332  	ctx, cancel := context.WithCancel(context.Background())
   333  
   334  	factory.Start(ctx.Done())
   335  	factory.WaitForCacheSync(ctx.Done())
   336  
   337  	env.mux = http.NewServeMux()
   338  	h := handler.NewOpenAPIService(&spec.Swagger{})
   339  	h.RegisterOpenAPIVersionedService("/openapi/v2", env.mux)
   340  
   341  	stopCh := make(chan struct{})
   342  
   343  	env.runFunc = func() {
   344  		go c.Run(&spec.Swagger{
   345  			SwaggerProps: spec.SwaggerProps{
   346  				Paths: &spec.Paths{
   347  					Paths: map[string]spec.PathItem{
   348  						"/apis/apiextensions.k8s.io/v1": {},
   349  					},
   350  				},
   351  			},
   352  		}, h, stopCh)
   353  	}
   354  
   355  	env.cleanFunc = func() {
   356  		cancel()
   357  		close(stopCh)
   358  	}
   359  	return env, ctx
   360  }
   361  
   362  func (t *testEnv) pollForCondition(conditionFunc func(*spec.Swagger) bool) {
   363  	wait.Poll(time.Second*1, wait.ForeverTestTimeout, func() (bool, error) {
   364  		openapi := t.fetchOpenAPIOrDie()
   365  		if conditionFunc(openapi) {
   366  			return true, nil
   367  		}
   368  		return false, nil
   369  	})
   370  }
   371  
   372  func (t *testEnv) pollForPathExists(path string) {
   373  	wait.Poll(time.Second*1, wait.ForeverTestTimeout, func() (bool, error) {
   374  		openapi := t.fetchOpenAPIOrDie()
   375  		if _, ok := openapi.Paths.Paths[path]; !ok {
   376  			return false, nil
   377  		}
   378  		return true, nil
   379  	})
   380  }
   381  
   382  func (t *testEnv) pollForPathNotExists(path string) {
   383  	wait.Poll(time.Second*1, wait.ForeverTestTimeout, func() (bool, error) {
   384  		openapi := t.fetchOpenAPIOrDie()
   385  		if _, ok := openapi.Paths.Paths[path]; ok {
   386  			return false, nil
   387  		}
   388  		return true, nil
   389  	})
   390  }
   391  
   392  func (t *testEnv) fetchOpenAPIOrDie() *spec.Swagger {
   393  	server := httptest.NewServer(t.mux)
   394  	defer server.Close()
   395  	client := server.Client()
   396  
   397  	req, err := http.NewRequest("GET", server.URL+"/openapi/v2", nil)
   398  	if err != nil {
   399  		t.t.Error(err)
   400  	}
   401  	resp, err := client.Do(req)
   402  	if err != nil {
   403  		t.t.Error(err)
   404  	}
   405  	body, err := io.ReadAll(resp.Body)
   406  	if err != nil {
   407  		t.t.Error(err)
   408  	}
   409  	swagger := &spec.Swagger{}
   410  	if err := swagger.UnmarshalJSON(body); err != nil {
   411  		t.t.Error(err)
   412  	}
   413  	return swagger
   414  }
   415  
   416  func (t *testEnv) expectPath(swagger *spec.Swagger, path string) {
   417  	if _, ok := swagger.Paths.Paths[path]; !ok {
   418  		t.t.Errorf("Expected path %s to exist in OpenAPI", path)
   419  	}
   420  }
   421  
   422  func (t *testEnv) expectNoPath(swagger *spec.Swagger, path string) {
   423  	if _, ok := swagger.Paths.Paths[path]; ok {
   424  		t.t.Errorf("Expected path %s to not exist in OpenAPI", path)
   425  	}
   426  }
   427  

View as plain text