k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/etcd/server.go (about) 1 /* 2 Copyright 2018 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 etcd 18 19 import ( 20 "context" 21 "encoding/json" 22 "net" 23 "net/http" 24 "os" 25 "strings" 26 "testing" 27 "time" 28 29 utiltesting "k8s.io/client-go/util/testing" 30 31 clientv3 "go.etcd.io/etcd/client/v3" 32 33 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 34 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 35 "k8s.io/apimachinery/pkg/api/meta" 36 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 37 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 38 "k8s.io/apimachinery/pkg/runtime" 39 "k8s.io/apimachinery/pkg/runtime/schema" 40 utilerrors "k8s.io/apimachinery/pkg/util/errors" 41 "k8s.io/apimachinery/pkg/util/wait" 42 genericapiserveroptions "k8s.io/apiserver/pkg/server/options" 43 cacheddiscovery "k8s.io/client-go/discovery/cached/memory" 44 "k8s.io/client-go/dynamic" 45 clientset "k8s.io/client-go/kubernetes" 46 restclient "k8s.io/client-go/rest" 47 "k8s.io/client-go/restmapper" 48 "k8s.io/kubernetes/cmd/kube-apiserver/app" 49 "k8s.io/kubernetes/cmd/kube-apiserver/app/options" 50 "k8s.io/kubernetes/test/integration" 51 "k8s.io/kubernetes/test/integration/framework" 52 "k8s.io/kubernetes/test/utils/ktesting" 53 netutils "k8s.io/utils/net" 54 55 // install all APIs 56 _ "k8s.io/kubernetes/pkg/controlplane" 57 ) 58 59 // This key is for testing purposes only and is not considered secure. 60 const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- 61 MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49 62 AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0 63 /IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== 64 -----END EC PRIVATE KEY-----` 65 66 // StartRealAPIServerOrDie starts an API server that is appropriate for use in tests that require one of every resource 67 func StartRealAPIServerOrDie(t *testing.T, configFuncs ...func(*options.ServerRunOptions)) *APIServer { 68 tCtx := ktesting.Init(t) 69 70 certDir, err := os.MkdirTemp("", t.Name()) 71 if err != nil { 72 t.Fatal(err) 73 } 74 75 _, defaultServiceClusterIPRange, err := netutils.ParseCIDRSloppy("10.0.0.0/24") 76 if err != nil { 77 t.Fatal(err) 78 } 79 80 listener, _, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0", net.ListenConfig{}) 81 if err != nil { 82 t.Fatal(err) 83 } 84 85 saSigningKeyFile, err := os.CreateTemp("/tmp", "insecure_test_key") 86 if err != nil { 87 t.Fatalf("create temp file failed: %v", err) 88 } 89 defer utiltesting.CloseAndRemove(t, saSigningKeyFile) 90 if err = os.WriteFile(saSigningKeyFile.Name(), []byte(ecdsaPrivateKey), 0666); err != nil { 91 t.Fatalf("write file %s failed: %v", saSigningKeyFile.Name(), err) 92 } 93 94 opts := options.NewServerRunOptions() 95 opts.Options.SecureServing.Listener = listener 96 opts.Options.SecureServing.ServerCert.CertDirectory = certDir 97 opts.Options.ServiceAccountSigningKeyFile = saSigningKeyFile.Name() 98 opts.Options.Etcd.StorageConfig.Transport.ServerList = []string{framework.GetEtcdURL()} 99 opts.Options.Etcd.DefaultStorageMediaType = runtime.ContentTypeJSON // force json we can easily interpret the result in etcd 100 opts.ServiceClusterIPRanges = defaultServiceClusterIPRange.String() 101 opts.Options.Authentication.APIAudiences = []string{"https://foo.bar.example.com"} 102 opts.Options.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"} 103 opts.Options.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()} 104 opts.Options.Authorization.Modes = []string{"RBAC"} 105 opts.Options.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 106 opts.Options.APIEnablement.RuntimeConfig["api/all"] = "true" 107 for _, f := range configFuncs { 108 f(opts) 109 } 110 completedOptions, err := opts.Complete() 111 if err != nil { 112 t.Fatal(err) 113 } 114 115 if errs := completedOptions.Validate(); len(errs) != 0 { 116 t.Fatalf("failed to validate ServerRunOptions: %v", utilerrors.NewAggregate(errs)) 117 } 118 119 // get etcd client before starting API server 120 rawClient, kvClient, err := integration.GetEtcdClients(completedOptions.Etcd.StorageConfig.Transport) 121 if err != nil { 122 t.Fatal(err) 123 } 124 125 // make sure we start with a clean slate 126 if _, err := kvClient.Delete(context.Background(), "/registry/", clientv3.WithPrefix()); err != nil { 127 t.Fatal(err) 128 } 129 130 config, err := app.NewConfig(completedOptions) 131 if err != nil { 132 t.Fatal(err) 133 } 134 completed, err := config.Complete() 135 if err != nil { 136 t.Fatal(err) 137 } 138 kubeAPIServer, err := app.CreateServerChain(completed) 139 if err != nil { 140 t.Fatal(err) 141 } 142 143 kubeClientConfig := restclient.CopyConfig(kubeAPIServer.GenericAPIServer.LoopbackClientConfig) 144 145 // we make lots of requests, don't be slow 146 kubeClientConfig.QPS = 99999 147 kubeClientConfig.Burst = 9999 148 149 // we make requests to all resources, don't log warnings about deprecated ones 150 restclient.SetDefaultWarningHandler(restclient.NoWarnings{}) 151 152 kubeClient := clientset.NewForConfigOrDie(kubeClientConfig) 153 154 errCh := make(chan error) 155 go func() { 156 // Catch panics that occur in this go routine so we get a comprehensible failure 157 defer func() { 158 if err := recover(); err != nil { 159 t.Errorf("Unexpected panic trying to start API server: %#v", err) 160 } 161 }() 162 defer close(errCh) 163 164 prepared, err := kubeAPIServer.PrepareRun() 165 if err != nil { 166 errCh <- err 167 return 168 } 169 if err := prepared.Run(tCtx); err != nil { 170 errCh <- err 171 t.Error(err) 172 return 173 } 174 }() 175 176 lastHealth := "" 177 attempt := 0 178 if err := wait.PollImmediate(time.Second, time.Minute, func() (done bool, err error) { 179 select { 180 case err := <-errCh: 181 return false, err 182 default: 183 } 184 185 // wait for the server to be healthy 186 result := kubeClient.RESTClient().Get().AbsPath("/healthz").Do(context.TODO()) 187 content, _ := result.Raw() 188 lastHealth = string(content) 189 if errResult := result.Error(); errResult != nil { 190 attempt++ 191 if attempt < 10 { 192 t.Log("waiting for server to be healthy") 193 } else { 194 t.Log(errResult) 195 } 196 return false, nil 197 } 198 var status int 199 result.StatusCode(&status) 200 return status == http.StatusOK, nil 201 }); err != nil { 202 t.Log(lastHealth) 203 t.Fatal(err) 204 } 205 206 // create CRDs so we can make sure that custom resources do not get lost 207 CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(kubeClientConfig), false, GetCustomResourceDefinitionData()...) 208 209 // force cached discovery reset 210 discoveryClient := cacheddiscovery.NewMemCacheClient(kubeClient.Discovery()) 211 restMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) 212 restMapper.Reset() 213 214 _, serverResources, err := kubeClient.Discovery().ServerGroupsAndResources() 215 if err != nil { 216 t.Fatal(err) 217 } 218 219 cleanup := func() { 220 // Cancel stopping apiserver and cleaning up 221 // after itself, including shutting down its storage layer. 222 tCtx.Cancel("cleaning up") 223 224 // If the apiserver was started, let's wait for it to 225 // shutdown clearly. 226 err, ok := <-errCh 227 if ok && err != nil { 228 t.Error(err) 229 } 230 rawClient.Close() 231 if err := os.RemoveAll(certDir); err != nil { 232 t.Log(err) 233 } 234 } 235 236 return &APIServer{ 237 Client: kubeClient, 238 Dynamic: dynamic.NewForConfigOrDie(kubeClientConfig), 239 Config: kubeClientConfig, 240 KV: kvClient, 241 Mapper: restMapper, 242 Resources: GetResources(t, serverResources), 243 Cleanup: cleanup, 244 } 245 } 246 247 // APIServer represents a running API server that is ready for use 248 // The Cleanup func must be deferred to prevent resource leaks 249 type APIServer struct { 250 Client clientset.Interface 251 Dynamic dynamic.Interface 252 Config *restclient.Config 253 KV clientv3.KV 254 Mapper meta.RESTMapper 255 Resources []Resource 256 Cleanup func() 257 } 258 259 // Resource contains REST mapping information for a specific resource and extra metadata such as delete collection support 260 type Resource struct { 261 Mapping *meta.RESTMapping 262 HasDeleteCollection bool 263 } 264 265 // GetResources fetches the Resources associated with serverResources that support get and create 266 func GetResources(t *testing.T, serverResources []*metav1.APIResourceList) []Resource { 267 var resources []Resource 268 269 for _, discoveryGroup := range serverResources { 270 for _, discoveryResource := range discoveryGroup.APIResources { 271 // this is a subresource, skip it 272 if strings.Contains(discoveryResource.Name, "/") { 273 continue 274 } 275 hasCreate := false 276 hasGet := false 277 hasDeleteCollection := false 278 for _, verb := range discoveryResource.Verbs { 279 if verb == "get" { 280 hasGet = true 281 } 282 if verb == "create" { 283 hasCreate = true 284 } 285 if verb == "deletecollection" { 286 hasDeleteCollection = true 287 } 288 } 289 if !(hasCreate && hasGet) { 290 continue 291 } 292 293 resourceGV, err := schema.ParseGroupVersion(discoveryGroup.GroupVersion) 294 if err != nil { 295 t.Fatal(err) 296 } 297 gvk := resourceGV.WithKind(discoveryResource.Kind) 298 if len(discoveryResource.Group) > 0 || len(discoveryResource.Version) > 0 { 299 gvk = schema.GroupVersionKind{ 300 Group: discoveryResource.Group, 301 Version: discoveryResource.Version, 302 Kind: discoveryResource.Kind, 303 } 304 } 305 gvr := resourceGV.WithResource(discoveryResource.Name) 306 307 resources = append(resources, Resource{ 308 Mapping: &meta.RESTMapping{ 309 Resource: gvr, 310 GroupVersionKind: gvk, 311 Scope: scope(discoveryResource.Namespaced), 312 }, 313 HasDeleteCollection: hasDeleteCollection, 314 }) 315 } 316 } 317 318 return resources 319 } 320 321 func scope(namespaced bool) meta.RESTScope { 322 if namespaced { 323 return meta.RESTScopeNamespace 324 } 325 return meta.RESTScopeRoot 326 } 327 328 // JSONToUnstructured converts a JSON stub to unstructured.Unstructured and 329 // returns a dynamic resource client that can be used to interact with it 330 func JSONToUnstructured(stub, namespace string, mapping *meta.RESTMapping, dynamicClient dynamic.Interface) (dynamic.ResourceInterface, *unstructured.Unstructured, error) { 331 typeMetaAdder := map[string]interface{}{} 332 if err := json.Unmarshal([]byte(stub), &typeMetaAdder); err != nil { 333 return nil, nil, err 334 } 335 336 // we don't require GVK on the data we provide, so we fill it in here. We could, but that seems extraneous. 337 typeMetaAdder["apiVersion"] = mapping.GroupVersionKind.GroupVersion().String() 338 typeMetaAdder["kind"] = mapping.GroupVersionKind.Kind 339 340 if mapping.Scope == meta.RESTScopeRoot { 341 namespace = "" 342 } 343 344 return dynamicClient.Resource(mapping.Resource).Namespace(namespace), &unstructured.Unstructured{Object: typeMetaAdder}, nil 345 } 346 347 // CreateTestCRDs creates the given CRDs, any failure causes the test to Fatal. 348 // If skipCrdExistsInDiscovery is true, the CRDs are only checked for the Established condition via their Status. 349 // If skipCrdExistsInDiscovery is false, the CRDs are checked via discovery, see CrdExistsInDiscovery. 350 func CreateTestCRDs(t *testing.T, client apiextensionsclientset.Interface, skipCrdExistsInDiscovery bool, crds ...*apiextensionsv1.CustomResourceDefinition) { 351 for _, crd := range crds { 352 createTestCRD(t, client, skipCrdExistsInDiscovery, crd) 353 } 354 } 355 356 func createTestCRD(t *testing.T, client apiextensionsclientset.Interface, skipCrdExistsInDiscovery bool, crd *apiextensionsv1.CustomResourceDefinition) { 357 if _, err := client.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}); err != nil { 358 t.Fatalf("Failed to create %s CRD; %v", crd.Name, err) 359 } 360 if skipCrdExistsInDiscovery { 361 if err := waitForEstablishedCRD(client, crd.Name); err != nil { 362 t.Fatalf("Failed to establish %s CRD; %v", crd.Name, err) 363 } 364 return 365 } 366 if err := wait.PollImmediate(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { 367 return CrdExistsInDiscovery(client, crd), nil 368 }); err != nil { 369 t.Fatalf("Failed to see %s in discovery: %v", crd.Name, err) 370 } 371 } 372 373 func waitForEstablishedCRD(client apiextensionsclientset.Interface, name string) error { 374 return wait.PollImmediate(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { 375 crd, err := client.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) 376 if err != nil { 377 return false, err 378 } 379 for _, cond := range crd.Status.Conditions { 380 switch cond.Type { 381 case apiextensionsv1.Established: 382 if cond.Status == apiextensionsv1.ConditionTrue { 383 return true, nil 384 } 385 } 386 } 387 return false, nil 388 }) 389 } 390 391 // CrdExistsInDiscovery checks to see if the given CRD exists in discovery at all served versions. 392 func CrdExistsInDiscovery(client apiextensionsclientset.Interface, crd *apiextensionsv1.CustomResourceDefinition) bool { 393 var versions []string 394 for _, v := range crd.Spec.Versions { 395 if v.Served { 396 versions = append(versions, v.Name) 397 } 398 } 399 for _, v := range versions { 400 if !crdVersionExistsInDiscovery(client, crd, v) { 401 return false 402 } 403 } 404 return true 405 } 406 407 func crdVersionExistsInDiscovery(client apiextensionsclientset.Interface, crd *apiextensionsv1.CustomResourceDefinition, version string) bool { 408 resourceList, err := client.Discovery().ServerResourcesForGroupVersion(crd.Spec.Group + "/" + version) 409 if err != nil { 410 return false 411 } 412 for _, resource := range resourceList.APIResources { 413 if resource.Name == crd.Spec.Names.Plural { 414 return true 415 } 416 } 417 return false 418 }