From 3180012c1bb8c68b742193d45ba928d05277af6b Mon Sep 17 00:00:00 2001 From: Kermit Alexander Date: Thu, 11 Nov 2021 19:27:11 +0000 Subject: [PATCH] Add integration tests for OpenAPI v3. --- test/integration/apiserver/openapiv3_test.go | 267 +++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 test/integration/apiserver/openapiv3_test.go diff --git a/test/integration/apiserver/openapiv3_test.go b/test/integration/apiserver/openapiv3_test.go new file mode 100644 index 00000000000..105da645963 --- /dev/null +++ b/test/integration/apiserver/openapiv3_test.go @@ -0,0 +1,267 @@ +/* +Copyright 2021 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 apiserver + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + "time" + + openapi_v3 "github.com/googleapis/gnostic/openapiv3" + "google.golang.org/protobuf/proto" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/test/integration/fixtures" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/client-go/dynamic" + kubernetes "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/kube-openapi/pkg/spec3" + apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/test/integration/framework" + "sigs.k8s.io/yaml" +) + +func TestOpenAPIV3SpecRoundTrip(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.OpenAPIV3, true)() + controlPlaneConfig := framework.NewIntegrationTestControlPlaneConfigWithOptions(&framework.ControlPlaneConfigOptions{}) + controlPlaneConfig.GenericConfig.OpenAPIConfig = framework.DefaultOpenAPIConfig() + instanceConfig, _, closeFn := framework.RunAnAPIServer(controlPlaneConfig) + defer closeFn() + paths := []string{ + "/apis/apps/v1", + "/apis/authentication.k8s.io/v1", + "/apis/policy/v1", + "/apis/batch/v1", + "/version", + } + for _, path := range paths { + t.Run(path, func(t *testing.T) { + rt, err := restclient.TransportFor(instanceConfig.GenericAPIServer.LoopbackClientConfig) + if err != nil { + t.Fatal(err) + } + // attempt to fetch and unmarshal + url := instanceConfig.GenericAPIServer.LoopbackClientConfig.Host + "/openapi/v3" + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Fatal(err) + } + resp, err := rt.RoundTrip(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + bs, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + var firstSpec spec3.OpenAPI + err = json.Unmarshal(bs, &firstSpec) + if err != nil { + t.Fatal(err) + } + specBytes, err := json.Marshal(&firstSpec) + if err != nil { + t.Fatal(err) + } + var secondSpec spec3.OpenAPI + err = json.Unmarshal(specBytes, &secondSpec) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(firstSpec, secondSpec) { + t.Fatal("spec mismatch") + } + }) + } +} + +func TestAddRemoveGroupVersion(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.OpenAPIV3, true)() + server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + config := server.ClientConfig + + apiExtensionClient, err := clientset.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + // Create a new CRD with group mygroup.example.com + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.NamespaceScoped) + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + + // It takes a second for CRD specs to propagate to the aggregator + time.Sleep(4 * time.Second) + + // Verify that the new group version is populated in the discovery for OpenaPI v3 + jsonData, err := clientset.RESTClient().Get().AbsPath("/openapi/v3").Do(context.TODO()).Raw() + if err != nil { + t.Fatal(err) + } + openAPIv3GV := make(map[string][]string) + err = json.Unmarshal(jsonData, &openAPIv3GV) + if err != nil { + t.Fatal(err) + } + + paths, ok := openAPIv3GV["Paths"] + if !ok { + t.Fatal("OpenAPI v3 format error") + } + + foundPath := false + for _, path := range paths { + if strings.Contains(path, "mygroup.example.com/v1beta1") { + foundPath = true + } + } + if foundPath == false { + t.Fatal("Expected group version mygroup.example.com to be present after CRD applied") + } + + // Check the spec for the newly published group version + jsonData, err = clientset.RESTClient().Get().AbsPath("/openapi/v3/apis/mygroup.example.com/v1beta1").Do(context.TODO()).Raw() + if err != nil { + t.Fatal(err) + } + var firstSpec spec3.OpenAPI + err = json.Unmarshal(jsonData, &firstSpec) + if err != nil { + t.Fatal(err) + } + + // Delete the CRD and ensure that the group/version is also deleted in discovery + if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } + time.Sleep(4 * time.Second) + + jsonData, err = clientset.RESTClient().Get().AbsPath("/openapi/v3").Do(context.TODO()).Raw() + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(jsonData, &openAPIv3GV) + if err != nil { + t.Fatal(err) + } + + paths, ok = openAPIv3GV["Paths"] + if !ok { + t.Fatal("OpenAPI v3 format error") + } + + for _, path := range paths { + if strings.Contains(path, "mygroup.example.com") { + t.Fatal("Unexpected group version mygroup.example.com in OpenAPI v3 discovery") + } + } +} + +func TestOpenAPIV3ProtoRoundtrip(t *testing.T) { + // The OpenAPI V3 proto library strips fields that are sibling elements to $ref + // See https://github.com/kubernetes/kubernetes/issues/106387 for more details + t.Skip("Skipping OpenAPI V3 Proto roundtrip test") + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.OpenAPIV3, true)() + controlPlaneConfig := framework.NewIntegrationTestControlPlaneConfigWithOptions(&framework.ControlPlaneConfigOptions{}) + controlPlaneConfig.GenericConfig.OpenAPIConfig = framework.DefaultOpenAPIConfig() + instanceConfig, _, closeFn := framework.RunAnAPIServer(controlPlaneConfig) + defer closeFn() + rt, err := restclient.TransportFor(instanceConfig.GenericAPIServer.LoopbackClientConfig) + if err != nil { + t.Fatal(err) + } + // attempt to fetch and unmarshal + req, err := http.NewRequest("GET", instanceConfig.GenericAPIServer.LoopbackClientConfig.Host+"/openapi/v3/apis/apps/v1", nil) + if err != nil { + t.Fatal(err) + } + resp, err := rt.RoundTrip(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + bs, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + var firstSpec spec3.OpenAPI + err = json.Unmarshal(bs, &firstSpec) + if err != nil { + t.Fatal(err) + } + + protoReq, err := http.NewRequest("GET", instanceConfig.GenericAPIServer.LoopbackClientConfig.Host+"/openapi/v3/apis/apps/v1", nil) + if err != nil { + t.Fatal(err) + } + protoReq.Header.Set("Accept", "application/com.github.proto-openapi.spec.v3@v1.0+protobuf") + protoResp, err := rt.RoundTrip(protoReq) + if err != nil { + t.Fatal(err) + } + defer protoResp.Body.Close() + bs, err = ioutil.ReadAll(protoResp.Body) + if err != nil { + t.Fatal(err) + } + var protoDoc openapi_v3.Document + err = proto.Unmarshal(bs, &protoDoc) + if err != nil { + t.Fatal(err) + } + + yamlBytes, err := protoDoc.YAMLValue("") + if err != nil { + t.Fatal(err) + } + jsonBytes, err := yaml.YAMLToJSON(yamlBytes) + if err != nil { + t.Fatal(err) + } + var specFromProto spec3.OpenAPI + err = json.Unmarshal(jsonBytes, &specFromProto) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(specFromProto, firstSpec) { + t.Fatalf("spec mismatch - specFromProto: %s\n", jsonBytes) + } +}