k8s.io/kubernetes@v1.29.3/test/integration/apiserver/openapi/openapiv3_test.go (about)

     1  /*
     2  Copyright 2021 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  	"encoding/json"
    22  	"io"
    23  	"net/http"
    24  	"reflect"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	openapi_v3 "github.com/google/gnostic-models/openapiv3"
    30  	"google.golang.org/protobuf/proto"
    31  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    32  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    33  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    34  	"k8s.io/client-go/dynamic"
    35  	kubernetes "k8s.io/client-go/kubernetes"
    36  	restclient "k8s.io/client-go/rest"
    37  	"k8s.io/kube-openapi/pkg/handler3"
    38  	"k8s.io/kube-openapi/pkg/spec3"
    39  	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    40  	"k8s.io/kubernetes/test/integration/framework"
    41  	"k8s.io/kubernetes/test/utils/ktesting"
    42  	"sigs.k8s.io/yaml"
    43  )
    44  
    45  func TestOpenAPIV3SpecRoundTrip(t *testing.T) {
    46  	_, ctx := ktesting.NewTestContext(t)
    47  	ctx, cancel := context.WithCancel(ctx)
    48  	defer cancel()
    49  
    50  	_, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{})
    51  	defer tearDownFn()
    52  
    53  	paths := []string{
    54  		"/apis/apps/v1",
    55  		"/apis/authentication.k8s.io/v1",
    56  		"/apis/policy/v1",
    57  		"/apis/batch/v1",
    58  		"/version",
    59  	}
    60  	for _, path := range paths {
    61  		t.Run(path, func(t *testing.T) {
    62  			rt, err := restclient.TransportFor(kubeConfig)
    63  			if err != nil {
    64  				t.Fatal(err)
    65  			}
    66  			// attempt to fetch and unmarshal
    67  			url := kubeConfig.Host + "/openapi/v3" + path
    68  			req, err := http.NewRequest("GET", url, nil)
    69  			if err != nil {
    70  				t.Fatal(err)
    71  			}
    72  			resp, err := rt.RoundTrip(req)
    73  			if err != nil {
    74  				t.Fatal(err)
    75  			}
    76  			defer resp.Body.Close()
    77  			bs, err := io.ReadAll(resp.Body)
    78  			if err != nil {
    79  				t.Fatal(err)
    80  			}
    81  			var firstSpec spec3.OpenAPI
    82  			err = json.Unmarshal(bs, &firstSpec)
    83  			if err != nil {
    84  				t.Fatal(err)
    85  			}
    86  			specBytes, err := json.Marshal(&firstSpec)
    87  			if err != nil {
    88  				t.Fatal(err)
    89  			}
    90  			var secondSpec spec3.OpenAPI
    91  			err = json.Unmarshal(specBytes, &secondSpec)
    92  			if err != nil {
    93  				t.Fatal(err)
    94  			}
    95  			if !reflect.DeepEqual(firstSpec, secondSpec) {
    96  				t.Fatal("spec mismatch")
    97  			}
    98  		})
    99  	}
   100  }
   101  
   102  func TestAddRemoveGroupVersion(t *testing.T) {
   103  	server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
   104  	if err != nil {
   105  		t.Fatal(err)
   106  	}
   107  	defer server.TearDownFn()
   108  	config := server.ClientConfig
   109  
   110  	apiExtensionClient, err := clientset.NewForConfig(config)
   111  	if err != nil {
   112  		t.Fatal(err)
   113  	}
   114  	dynamicClient, err := dynamic.NewForConfig(config)
   115  	if err != nil {
   116  		t.Fatal(err)
   117  	}
   118  	clientset, err := kubernetes.NewForConfig(config)
   119  	if err != nil {
   120  		t.Fatal(err)
   121  	}
   122  
   123  	// Create a new CRD with group mygroup.example.com
   124  	noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.NamespaceScoped)
   125  	noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   126  	if err != nil {
   127  		t.Fatal(err)
   128  	}
   129  
   130  	// It takes a second for CRD specs to propagate to the aggregator
   131  	time.Sleep(4 * time.Second)
   132  
   133  	// Verify that the new group version is populated in the discovery for OpenaPI v3
   134  	jsonData, err := clientset.RESTClient().Get().AbsPath("/openapi/v3").Do(context.TODO()).Raw()
   135  	if err != nil {
   136  		t.Fatal(err)
   137  	}
   138  	openAPIv3GV := handler3.OpenAPIV3Discovery{}
   139  	err = json.Unmarshal(jsonData, &openAPIv3GV)
   140  	if err != nil {
   141  		t.Fatal(err)
   142  	}
   143  	foundPath := false
   144  	for path := range openAPIv3GV.Paths {
   145  		if strings.Contains(path, "mygroup.example.com/v1beta1") {
   146  			foundPath = true
   147  		}
   148  	}
   149  	if foundPath == false {
   150  		t.Fatal("Expected group version mygroup.example.com to be present after CRD applied")
   151  	}
   152  
   153  	// Check the spec for the newly published group version
   154  	jsonData, err = clientset.RESTClient().Get().AbsPath("/openapi/v3/apis/mygroup.example.com/v1beta1").Do(context.TODO()).Raw()
   155  	if err != nil {
   156  		t.Fatal(err)
   157  	}
   158  	var firstSpec spec3.OpenAPI
   159  	err = json.Unmarshal(jsonData, &firstSpec)
   160  	if err != nil {
   161  		t.Fatal(err)
   162  	}
   163  
   164  	// Delete the CRD and ensure that the group/version is also deleted in discovery
   165  	if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   166  		t.Fatal(err)
   167  	}
   168  	time.Sleep(4 * time.Second)
   169  
   170  	jsonData, err = clientset.RESTClient().Get().AbsPath("/openapi/v3").Do(context.TODO()).Raw()
   171  	if err != nil {
   172  		t.Fatal(err)
   173  	}
   174  	openAPIv3GV = handler3.OpenAPIV3Discovery{}
   175  	err = json.Unmarshal(jsonData, &openAPIv3GV)
   176  	if err != nil {
   177  		t.Fatal(err)
   178  	}
   179  
   180  	for path := range openAPIv3GV.Paths {
   181  		if strings.Contains(path, "mygroup.example.com") {
   182  			t.Fatal("Unexpected group version mygroup.example.com in OpenAPI v3 discovery")
   183  		}
   184  	}
   185  }
   186  
   187  func TestOpenAPIV3ProtoRoundtrip(t *testing.T) {
   188  	// The OpenAPI V3 proto library strips fields that are sibling elements to $ref
   189  	// See https://github.com/kubernetes/kubernetes/issues/106387 for more details
   190  	t.Skip("Skipping OpenAPI V3 Proto roundtrip test")
   191  
   192  	_, ctx := ktesting.NewTestContext(t)
   193  	ctx, cancel := context.WithCancel(ctx)
   194  	defer cancel()
   195  
   196  	_, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{})
   197  	defer tearDownFn()
   198  
   199  	rt, err := restclient.TransportFor(kubeConfig)
   200  	if err != nil {
   201  		t.Fatal(err)
   202  	}
   203  	// attempt to fetch and unmarshal
   204  	req, err := http.NewRequest("GET", kubeConfig.Host+"/openapi/v3/apis/apps/v1", nil)
   205  	if err != nil {
   206  		t.Fatal(err)
   207  	}
   208  	resp, err := rt.RoundTrip(req)
   209  	if err != nil {
   210  		t.Fatal(err)
   211  	}
   212  	defer resp.Body.Close()
   213  	bs, err := io.ReadAll(resp.Body)
   214  	if err != nil {
   215  		t.Fatal(err)
   216  	}
   217  	var firstSpec spec3.OpenAPI
   218  	err = json.Unmarshal(bs, &firstSpec)
   219  	if err != nil {
   220  		t.Fatal(err)
   221  	}
   222  
   223  	protoReq, err := http.NewRequest("GET", kubeConfig.Host+"/openapi/v3/apis/apps/v1", nil)
   224  	if err != nil {
   225  		t.Fatal(err)
   226  	}
   227  	protoReq.Header.Set("Accept", "application/com.github.proto-openapi.spec.v3@v1.0+protobuf")
   228  	protoResp, err := rt.RoundTrip(protoReq)
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  	defer protoResp.Body.Close()
   233  	bs, err = io.ReadAll(protoResp.Body)
   234  	if err != nil {
   235  		t.Fatal(err)
   236  	}
   237  	var protoDoc openapi_v3.Document
   238  	err = proto.Unmarshal(bs, &protoDoc)
   239  	if err != nil {
   240  		t.Fatal(err)
   241  	}
   242  
   243  	yamlBytes, err := protoDoc.YAMLValue("")
   244  	if err != nil {
   245  		t.Fatal(err)
   246  	}
   247  	jsonBytes, err := yaml.YAMLToJSON(yamlBytes)
   248  	if err != nil {
   249  		t.Fatal(err)
   250  	}
   251  	var specFromProto spec3.OpenAPI
   252  	err = json.Unmarshal(jsonBytes, &specFromProto)
   253  	if err != nil {
   254  		t.Fatal(err)
   255  	}
   256  
   257  	if !reflect.DeepEqual(specFromProto, firstSpec) {
   258  		t.Fatalf("spec mismatch - specFromProto: %s\n", jsonBytes)
   259  	}
   260  }