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