github.com/cilium/cilium@v1.16.2/pkg/k8s/client/cell.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package client 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "net" 11 "net/http" 12 "os" 13 "path/filepath" 14 "strings" 15 "time" 16 17 "github.com/cilium/hive/cell" 18 "github.com/sirupsen/logrus" 19 apiext_clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 20 apiext_fake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" 21 k8sErrors "k8s.io/apimachinery/pkg/api/errors" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 utilnet "k8s.io/apimachinery/pkg/util/net" 24 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 25 "k8s.io/apimachinery/pkg/util/wait" 26 "k8s.io/client-go/discovery" 27 "k8s.io/client-go/kubernetes" 28 "k8s.io/client-go/kubernetes/fake" 29 "k8s.io/client-go/rest" 30 "k8s.io/client-go/tools/clientcmd" 31 "k8s.io/client-go/util/connrotation" 32 33 "github.com/cilium/cilium/pkg/controller" 34 cilium_clientset "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned" 35 cilium_fake "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/fake" 36 k8smetrics "github.com/cilium/cilium/pkg/k8s/metrics" 37 slim_apiextclientsetscheme "github.com/cilium/cilium/pkg/k8s/slim/k8s/apiextensions-client/clientset/versioned/scheme" 38 slim_apiext_clientset "github.com/cilium/cilium/pkg/k8s/slim/k8s/apiextensions-clientset" 39 slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 40 slim_metav1beta1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1beta1" 41 slim_clientset "github.com/cilium/cilium/pkg/k8s/slim/k8s/client/clientset/versioned" 42 slim_fake "github.com/cilium/cilium/pkg/k8s/slim/k8s/client/clientset/versioned/fake" 43 k8sversion "github.com/cilium/cilium/pkg/k8s/version" 44 "github.com/cilium/cilium/pkg/logging/logfields" 45 "github.com/cilium/cilium/pkg/version" 46 ) 47 48 // client.Cell provides Clientset, a composition of clientsets to Kubernetes resources 49 // used by Cilium. 50 var Cell = cell.Module( 51 "k8s-client", 52 "Kubernetes Client", 53 54 cell.Config(defaultConfig), 55 cell.Provide(newClientset), 56 ) 57 58 // client.ClientBuilderCell provides a function to create a new composite Clientset, 59 // allowing a controller to use its own Clientset with a different user agent. 60 var ClientBuilderCell = cell.Module( 61 "k8s-client-builder", 62 "Kubernetes Client Builder", 63 64 cell.Provide(NewClientBuilder), 65 ) 66 67 var k8sHeartbeatControllerGroup = controller.NewGroup("k8s-heartbeat") 68 69 // Type aliases for the clientsets to avoid name collision on 'Clientset' when composing them. 70 type ( 71 KubernetesClientset = kubernetes.Clientset 72 SlimClientset = slim_clientset.Clientset 73 APIExtClientset = slim_apiext_clientset.Clientset 74 CiliumClientset = cilium_clientset.Clientset 75 ) 76 77 // Clientset is a composition of the different client sets used by Cilium. 78 type Clientset interface { 79 kubernetes.Interface 80 apiext_clientset.Interface 81 cilium_clientset.Interface 82 Getters 83 84 // Slim returns the slim client, which contains some of the same APIs as the 85 // normal kubernetes client, but with slimmed down messages to reduce memory 86 // usage. Prefer the slim version when caching messages. 87 Slim() slim_clientset.Interface 88 89 // IsEnabled returns true if Kubernetes support is enabled and the 90 // clientset can be used. 91 IsEnabled() bool 92 93 // Disable disables the client. Panics if called after the clientset has been 94 // started. 95 Disable() 96 97 // Config returns the configuration used to create this client. 98 Config() Config 99 100 // RestConfig returns the deep copy of rest configuration. 101 RestConfig() *rest.Config 102 } 103 104 // compositeClientset implements the Clientset using real clients. 105 type compositeClientset struct { 106 started bool 107 disabled bool 108 109 *KubernetesClientset 110 *APIExtClientset 111 *CiliumClientset 112 clientsetGetters 113 114 controller *controller.Manager 115 slim *SlimClientset 116 config Config 117 log logrus.FieldLogger 118 closeAllConns func() 119 restConfig *rest.Config 120 } 121 122 func newClientset(lc cell.Lifecycle, log logrus.FieldLogger, cfg Config) (Clientset, error) { 123 return newClientsetForUserAgent(lc, log, cfg, "") 124 } 125 126 func newClientsetForUserAgent(lc cell.Lifecycle, log logrus.FieldLogger, cfg Config, name string) (Clientset, error) { 127 if !cfg.isEnabled() { 128 return &compositeClientset{disabled: true}, nil 129 } 130 131 if cfg.K8sAPIServer != "" && 132 !strings.HasPrefix(cfg.K8sAPIServer, "http") { 133 cfg.K8sAPIServer = "http://" + cfg.K8sAPIServer // default to HTTP 134 } 135 136 client := compositeClientset{ 137 log: log, 138 controller: controller.NewManager(), 139 config: cfg, 140 } 141 142 cmdName := "cilium" 143 if len(os.Args[0]) != 0 { 144 cmdName = filepath.Base(os.Args[0]) 145 } 146 userAgent := fmt.Sprintf("%s/%s", cmdName, version.Version) 147 148 if name != "" { 149 userAgent = fmt.Sprintf("%s %s", userAgent, name) 150 } 151 152 restConfig, err := createConfig(cfg.K8sAPIServer, cfg.K8sKubeConfigPath, cfg.K8sClientQPS, cfg.K8sClientBurst, userAgent) 153 if err != nil { 154 return nil, fmt.Errorf("unable to create k8s client rest configuration: %w", err) 155 } 156 client.restConfig = restConfig 157 defaultCloseAllConns := setDialer(cfg, restConfig) 158 159 httpClient, err := rest.HTTPClientFor(restConfig) 160 if err != nil { 161 return nil, fmt.Errorf("unable to create k8s REST client: %w", err) 162 } 163 164 // We are implementing the same logic as Kubelet, see 165 // https://github.com/kubernetes/kubernetes/blob/v1.24.0-beta.0/cmd/kubelet/app/server.go#L852. 166 if s := os.Getenv("DISABLE_HTTP2"); len(s) > 0 { 167 client.closeAllConns = defaultCloseAllConns 168 } else { 169 client.closeAllConns = func() { 170 utilnet.CloseIdleConnectionsFor(restConfig.Transport) 171 } 172 } 173 174 // Slim and K8s clients use protobuf marshalling. 175 restConfig.ContentConfig.ContentType = `application/vnd.kubernetes.protobuf` 176 177 client.slim, err = slim_clientset.NewForConfigAndClient(restConfig, httpClient) 178 if err != nil { 179 return nil, fmt.Errorf("unable to create slim k8s client: %w", err) 180 } 181 182 client.APIExtClientset, err = slim_apiext_clientset.NewForConfigAndClient(restConfig, httpClient) 183 if err != nil { 184 return nil, fmt.Errorf("unable to create apiext k8s client: %w", err) 185 } 186 187 client.KubernetesClientset, err = kubernetes.NewForConfigAndClient(restConfig, httpClient) 188 if err != nil { 189 return nil, fmt.Errorf("unable to create k8s client: %w", err) 190 } 191 192 client.clientsetGetters = clientsetGetters{&client} 193 194 // The cilium client uses JSON marshalling. 195 restConfig.ContentConfig.ContentType = `application/json` 196 client.CiliumClientset, err = cilium_clientset.NewForConfigAndClient(restConfig, httpClient) 197 if err != nil { 198 return nil, fmt.Errorf("unable to create cilium k8s client: %w", err) 199 } 200 201 lc.Append(cell.Hook{ 202 OnStart: client.onStart, 203 OnStop: client.onStop, 204 }) 205 206 return &client, nil 207 } 208 209 func (c *compositeClientset) Slim() slim_clientset.Interface { 210 return c.slim 211 } 212 213 func (c *compositeClientset) Discovery() discovery.DiscoveryInterface { 214 return c.KubernetesClientset.Discovery() 215 } 216 217 func (c *compositeClientset) IsEnabled() bool { 218 return c != nil && c.config.isEnabled() && !c.disabled 219 } 220 221 func (c *compositeClientset) Disable() { 222 if c.started { 223 panic("Clientset.Disable() called after it had been started") 224 } 225 c.disabled = true 226 } 227 228 func (c *compositeClientset) Config() Config { 229 return c.config 230 } 231 232 func (c *compositeClientset) RestConfig() *rest.Config { 233 return rest.CopyConfig(c.restConfig) 234 } 235 236 func (c *compositeClientset) onStart(startCtx cell.HookContext) error { 237 if !c.IsEnabled() { 238 return nil 239 } 240 241 if err := c.waitForConn(startCtx); err != nil { 242 return err 243 } 244 c.startHeartbeat() 245 246 // Update the global K8s clients, K8s version and the capabilities. 247 if err := k8sversion.Update(c, c.config.EnableK8sAPIDiscovery); err != nil { 248 return err 249 } 250 251 if !k8sversion.Capabilities().MinimalVersionMet { 252 return fmt.Errorf("k8s version (%v) is not meeting the minimal requirement (%v)", 253 k8sversion.Version(), k8sversion.MinimalVersionConstraint) 254 } 255 256 c.started = true 257 258 return nil 259 } 260 261 func (c *compositeClientset) onStop(stopCtx cell.HookContext) error { 262 if c.IsEnabled() { 263 c.controller.RemoveAllAndWait() 264 c.closeAllConns() 265 } 266 c.started = false 267 return nil 268 } 269 270 func (c *compositeClientset) startHeartbeat() { 271 restClient := c.KubernetesClientset.RESTClient() 272 273 timeout := c.config.K8sHeartbeatTimeout 274 if timeout == 0 { 275 return 276 } 277 278 heartBeat := func(ctx context.Context) error { 279 // Kubernetes does a get node of the node that kubelet is running [0]. This seems excessive in 280 // our case because the amount of data transferred is bigger than doing a Get of /healthz. 281 // For this reason we have picked to perform a get on `/healthz` instead a get of a node. 282 // 283 // [0] https://github.com/kubernetes/kubernetes/blob/v1.17.3/pkg/kubelet/kubelet_node_status.go#L423 284 res := restClient.Get().Resource("healthz").Do(ctx) 285 return res.Error() 286 } 287 288 c.controller.UpdateController("k8s-heartbeat", 289 controller.ControllerParams{ 290 Group: k8sHeartbeatControllerGroup, 291 DoFunc: func(context.Context) error { 292 runHeartbeat( 293 c.log, 294 heartBeat, 295 timeout, 296 c.closeAllConns, 297 ) 298 return nil 299 }, 300 RunInterval: timeout, 301 }) 302 } 303 304 // createConfig creates a rest.Config for connecting to k8s api-server. 305 // 306 // The precedence of the configuration selection is the following: 307 // 1. kubeCfgPath 308 // 2. apiServerURL (https if specified) 309 // 3. rest.InClusterConfig(). 310 func createConfig(apiServerURL, kubeCfgPath string, qps float32, burst int, userAgent string) (*rest.Config, error) { 311 var ( 312 config *rest.Config 313 err error 314 ) 315 316 switch { 317 // If the apiServerURL and the kubeCfgPath are empty then we can try getting 318 // the rest.Config from the InClusterConfig 319 case apiServerURL == "" && kubeCfgPath == "": 320 if config, err = rest.InClusterConfig(); err != nil { 321 return nil, err 322 } 323 case kubeCfgPath != "": 324 if config, err = clientcmd.BuildConfigFromFlags("", kubeCfgPath); err != nil { 325 return nil, err 326 } 327 case strings.HasPrefix(apiServerURL, "https://"): 328 if config, err = rest.InClusterConfig(); err != nil { 329 return nil, err 330 } 331 config.Host = apiServerURL 332 default: 333 config = &rest.Config{Host: apiServerURL, UserAgent: userAgent} 334 } 335 336 setConfig(config, userAgent, qps, burst) 337 return config, nil 338 } 339 340 func setConfig(config *rest.Config, userAgent string, qps float32, burst int) { 341 if userAgent != "" { 342 config.UserAgent = userAgent 343 } 344 if qps != 0.0 { 345 config.QPS = qps 346 } 347 if burst != 0 { 348 config.Burst = burst 349 } 350 } 351 352 func (c *compositeClientset) waitForConn(ctx context.Context) error { 353 stop := make(chan struct{}) 354 timeout := time.NewTimer(time.Minute) 355 defer timeout.Stop() 356 var err error 357 wait.Until(func() { 358 c.log.WithField("host", c.restConfig.Host).Info("Establishing connection to apiserver") 359 err = isConnReady(c) 360 if err == nil { 361 close(stop) 362 return 363 } 364 365 select { 366 case <-ctx.Done(): 367 case <-timeout.C: 368 default: 369 return 370 } 371 372 c.log.WithError(err).WithField(logfields.IPAddr, c.restConfig.Host).Error("Unable to contact k8s api-server") 373 close(stop) 374 }, 5*time.Second, stop) 375 if err == nil { 376 c.log.Info("Connected to apiserver") 377 } 378 return err 379 } 380 381 func setDialer(cfg Config, restConfig *rest.Config) func() { 382 if cfg.K8sClientConnectionTimeout == 0 || cfg.K8sClientConnectionKeepAlive == 0 { 383 return func() {} 384 } 385 ctx := (&net.Dialer{ 386 Timeout: cfg.K8sClientConnectionTimeout, 387 KeepAlive: cfg.K8sClientConnectionKeepAlive, 388 }).DialContext 389 dialer := connrotation.NewDialer(ctx) 390 restConfig.Dial = dialer.DialContext 391 return dialer.CloseAll 392 } 393 394 func runHeartbeat(log logrus.FieldLogger, heartBeat func(context.Context) error, timeout time.Duration, closeAllConns ...func()) { 395 expireDate := time.Now().Add(-timeout) 396 // Don't even perform a health check if we have received a successful 397 // k8s event in the last 'timeout' duration 398 if k8smetrics.LastSuccessInteraction.Time().After(expireDate) { 399 return 400 } 401 402 done := make(chan error) 403 ctx, cancel := context.WithTimeout(context.Background(), timeout) 404 defer cancel() 405 go func() { 406 // If we have reached up to this point to perform a heartbeat to 407 // kube-apiserver then we should close the connections if we receive 408 // any error at all except if we receive a http.StatusTooManyRequests 409 // which means the server is overloaded and only for this reason we 410 // will not close all connections. 411 err := heartBeat(ctx) 412 if err != nil { 413 statusError := &k8sErrors.StatusError{} 414 if !errors.As(err, &statusError) || 415 statusError.ErrStatus.Code != http.StatusTooManyRequests { 416 done <- err 417 } 418 } 419 close(done) 420 }() 421 422 select { 423 case err := <-done: 424 if err != nil { 425 log.WithError(err).Warn("Network status error received, restarting client connections") 426 for _, fn := range closeAllConns { 427 fn() 428 } 429 } 430 case <-ctx.Done(): 431 log.Warn("Heartbeat timed out, restarting client connections") 432 for _, fn := range closeAllConns { 433 fn() 434 } 435 } 436 } 437 438 // isConnReady returns the err for the kube-system namespace get 439 func isConnReady(c kubernetes.Interface) error { 440 _, err := c.CoreV1().Namespaces().Get(context.TODO(), "kube-system", metav1.GetOptions{}) 441 return err 442 } 443 444 var FakeClientCell = cell.Provide(NewFakeClientset) 445 446 type ( 447 KubernetesFakeClientset = fake.Clientset 448 SlimFakeClientset = slim_fake.Clientset 449 CiliumFakeClientset = cilium_fake.Clientset 450 APIExtFakeClientset = apiext_fake.Clientset 451 ) 452 453 type FakeClientset struct { 454 disabled bool 455 456 *KubernetesFakeClientset 457 *CiliumFakeClientset 458 *APIExtFakeClientset 459 clientsetGetters 460 461 SlimFakeClientset *SlimFakeClientset 462 463 enabled bool 464 } 465 466 var _ Clientset = &FakeClientset{} 467 468 func (c *FakeClientset) Slim() slim_clientset.Interface { 469 return c.SlimFakeClientset 470 } 471 472 func (c *FakeClientset) Discovery() discovery.DiscoveryInterface { 473 return c.KubernetesFakeClientset.Discovery() 474 } 475 476 func (c *FakeClientset) IsEnabled() bool { 477 return !c.disabled 478 } 479 480 func (c *FakeClientset) Disable() { 481 c.disabled = true 482 } 483 484 func (c *FakeClientset) Config() Config { 485 return Config{} 486 } 487 488 func (c *FakeClientset) RestConfig() *rest.Config { 489 return &rest.Config{} 490 } 491 492 func NewFakeClientset() (*FakeClientset, Clientset) { 493 client := FakeClientset{ 494 SlimFakeClientset: slim_fake.NewSimpleClientset(), 495 CiliumFakeClientset: cilium_fake.NewSimpleClientset(), 496 APIExtFakeClientset: apiext_fake.NewSimpleClientset(), 497 KubernetesFakeClientset: fake.NewSimpleClientset(), 498 enabled: true, 499 } 500 client.clientsetGetters = clientsetGetters{&client} 501 return &client, &client 502 } 503 504 type ClientBuilderFunc func(name string) (Clientset, error) 505 506 // NewClientBuilder returns a function that creates a new Clientset with the given 507 // name appended to the user agent, or returns an error if the Clientset cannot be 508 // created. 509 func NewClientBuilder(lc cell.Lifecycle, log logrus.FieldLogger, cfg Config) ClientBuilderFunc { 510 return func(name string) (Clientset, error) { 511 c, err := newClientsetForUserAgent(lc, log, cfg, name) 512 if err != nil { 513 return nil, err 514 } 515 return c, nil 516 } 517 } 518 519 var FakeClientBuilderCell = cell.Provide(FakeClientBuilder) 520 521 func FakeClientBuilder() ClientBuilderFunc { 522 fc, _ := NewFakeClientset() 523 return func(_ string) (Clientset, error) { 524 return fc, nil 525 } 526 } 527 528 func init() { 529 // Register the metav1.Table and metav1.PartialObjectMetadata for the 530 // apiextclientset. 531 utilruntime.Must(slim_metav1.AddMetaToScheme(slim_apiextclientsetscheme.Scheme)) 532 utilruntime.Must(slim_metav1beta1.AddMetaToScheme(slim_apiextclientsetscheme.Scheme)) 533 }