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  }