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 }