github.com/cilium/cilium@v1.16.2/pkg/k8s/synced/crd.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  // Package synced provides tools for tracking if k8s resources have
     5  // been initially sychronized with the k8s apiserver.
     6  package synced
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  
    12  	apiextclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/fields"
    15  	"k8s.io/apimachinery/pkg/runtime"
    16  	"k8s.io/apimachinery/pkg/watch"
    17  	"k8s.io/client-go/rest"
    18  	"k8s.io/client-go/tools/cache"
    19  
    20  	v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    21  	v2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    22  	"github.com/cilium/cilium/pkg/k8s/client"
    23  	"github.com/cilium/cilium/pkg/k8s/informer"
    24  	slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
    25  	"github.com/cilium/cilium/pkg/lock"
    26  	"github.com/cilium/cilium/pkg/option"
    27  	"github.com/cilium/cilium/pkg/time"
    28  )
    29  
    30  const (
    31  	k8sAPIGroupCRD = "CustomResourceDefinition"
    32  )
    33  
    34  func CRDResourceName(crd string) string {
    35  	return "crd:" + crd
    36  }
    37  
    38  func agentCRDResourceNames() []string {
    39  	result := []string{
    40  		CRDResourceName(v2.CNPName),
    41  		CRDResourceName(v2.CCNPName),
    42  		CRDResourceName(v2.CNName),
    43  		CRDResourceName(v2.CIDName),
    44  		CRDResourceName(v2alpha1.CCGName),
    45  		CRDResourceName(v2alpha1.CPIPName),
    46  	}
    47  
    48  	if !option.Config.DisableCiliumEndpointCRD {
    49  		result = append(result, CRDResourceName(v2.CEPName))
    50  		if option.Config.EnableCiliumEndpointSlice {
    51  			result = append(result, CRDResourceName(v2alpha1.CESName))
    52  		}
    53  	}
    54  
    55  	if option.Config.EnableIPv4EgressGateway {
    56  		result = append(result, CRDResourceName(v2.CEGPName))
    57  	}
    58  	if option.Config.EnableLocalRedirectPolicy {
    59  		result = append(result, CRDResourceName(v2.CLRPName))
    60  	}
    61  	if option.Config.EnableEnvoyConfig {
    62  		result = append(result, CRDResourceName(v2.CCECName))
    63  		result = append(result, CRDResourceName(v2.CECName))
    64  	}
    65  	if option.Config.EnableBGPControlPlane {
    66  		result = append(result, CRDResourceName(v2alpha1.BGPPName))
    67  		// BGPv2 CRDs
    68  		result = append(result, CRDResourceName(v2alpha1.BGPCCName))
    69  		result = append(result, CRDResourceName(v2alpha1.BGPAName))
    70  		result = append(result, CRDResourceName(v2alpha1.BGPPCName))
    71  		result = append(result, CRDResourceName(v2alpha1.BGPNCName))
    72  		result = append(result, CRDResourceName(v2alpha1.BGPNCOName))
    73  	}
    74  
    75  	result = append(result,
    76  		CRDResourceName(v2alpha1.LBIPPoolName),
    77  		CRDResourceName(v2alpha1.L2AnnouncementName),
    78  	)
    79  
    80  	return result
    81  }
    82  
    83  // AgentCRDResourceNames returns a list of all CRD resource names the Cilium
    84  // agent needs to wait to be registered before initializing any k8s watchers.
    85  func AgentCRDResourceNames() []string {
    86  	return agentCRDResourceNames()
    87  }
    88  
    89  // ClusterMeshAPIServerResourceNames returns a list of all CRD resource names the
    90  // clustermesh-apiserver needs to wait to be registered before initializing any
    91  // k8s watchers.
    92  func ClusterMeshAPIServerResourceNames() []string {
    93  	return []string{
    94  		CRDResourceName(v2.CNName),
    95  		CRDResourceName(v2.CIDName),
    96  		CRDResourceName(v2.CEPName),
    97  		CRDResourceName(v2.CEWName),
    98  	}
    99  }
   100  
   101  // AllCiliumCRDResourceNames returns a list of all Cilium CRD resource names
   102  // that the cilium operator or testsuite may register.
   103  func AllCiliumCRDResourceNames() []string {
   104  	return append(
   105  		AgentCRDResourceNames(),
   106  		CRDResourceName(v2.CEWName),
   107  		CRDResourceName(v2.CNCName),
   108  		CRDResourceName(v2alpha1.CNCName), // TODO depreciate CNC on v2alpha1 https://github.com/cilium/cilium/issues/31982
   109  	)
   110  }
   111  
   112  // SyncCRDs will sync Cilium CRDs to ensure that they have all been
   113  // installed inside the K8s cluster. These CRDs are added by the
   114  // Cilium Operator. This function will block until it finds all the
   115  // CRDs or if a timeout occurs.
   116  func SyncCRDs(ctx context.Context, clientset client.Clientset, crdNames []string, rs *Resources, ag *APIGroups) error {
   117  	crds := newCRDState(crdNames)
   118  
   119  	listerWatcher := newListWatchFromClient(
   120  		newCRDGetter(clientset),
   121  		fields.Everything(),
   122  	)
   123  	_, crdController := informer.NewInformer(
   124  		listerWatcher,
   125  		&slim_metav1.PartialObjectMetadata{},
   126  		0,
   127  		cache.ResourceEventHandlerFuncs{
   128  			AddFunc:    func(obj interface{}) { crds.add(obj) },
   129  			DeleteFunc: func(obj interface{}) { crds.remove(obj) },
   130  		},
   131  		nil,
   132  	)
   133  
   134  	// Create a context so that we can timeout after the configured CRD wait
   135  	// peroid.
   136  	ctx, cancel := context.WithTimeout(ctx, option.Config.CRDWaitTimeout)
   137  	defer cancel()
   138  
   139  	crds.Lock()
   140  	for crd := range crds.m {
   141  		rs.BlockWaitGroupToSyncResources(
   142  			ctx.Done(),
   143  			nil,
   144  			func() bool {
   145  				crds.Lock()
   146  				defer crds.Unlock()
   147  				return crds.m[crd]
   148  			},
   149  			crd,
   150  		)
   151  	}
   152  	crds.Unlock()
   153  
   154  	// The above loop will call blockWaitGroupToSyncResources to populate the
   155  	// K8sWatcher state with the current state of the CRDs. It will check the
   156  	// state of each CRD, with the inline function provided. If the function
   157  	// reports that the given CRD is true (has been synced), it will close a
   158  	// channel associated with the given CRD. A subsequent call to
   159  	// (*K8sWatcher).WaitForCacheSync will notice that a given CRD's channel
   160  	// has been closed. Once all the CRDs passed to WaitForCacheSync have had
   161  	// their channels closed, the function unblocks.
   162  	//
   163  	// Meanwhile, the below code kicks off the controller that was instantiated
   164  	// above, and enters a loop looking for (1) if the context has deadlined or
   165  	// (2) if the entire CRD state has been synced (all CRDs found in the
   166  	// cluster). While we're in for-select loop, the controller is listening
   167  	// for either add or delete events to the customresourcedefinition resource
   168  	// (disguised inside a metav1.PartialObjectMetadata object). If (1) is
   169  	// encountered, then Cilium will fatal because it cannot proceed if the
   170  	// CRDs are not present. If (2) is encountered, then make sure the
   171  	// controller has exited by cancelling the context and we return out.
   172  
   173  	go crdController.Run(ctx.Done())
   174  	ag.AddAPI(k8sAPIGroupCRD)
   175  	// We no longer need this API to show up in `cilium status` as the
   176  	// controller will exit after this function.
   177  	defer ag.RemoveAPI(k8sAPIGroupCRD)
   178  
   179  	log.Info("Waiting until all Cilium CRDs are available")
   180  
   181  	ticker := time.NewTicker(50 * time.Millisecond)
   182  	count := 0
   183  	for {
   184  		select {
   185  		case <-ctx.Done():
   186  			err := ctx.Err()
   187  			if err != nil && !errors.Is(err, context.Canceled) {
   188  				log.WithError(err).
   189  					Fatalf("Unable to find all Cilium CRDs necessary within "+
   190  						"%v timeout. Please ensure that Cilium Operator is "+
   191  						"running, as it's responsible for registering all "+
   192  						"the Cilium CRDs. The following CRDs were not found: %v",
   193  						option.Config.CRDWaitTimeout, crds.unSynced())
   194  			}
   195  			// If the context was canceled it means the daemon is being stopped
   196  			// so we can return the context's error.
   197  			return err
   198  		case <-ticker.C:
   199  			if crds.isSynced() {
   200  				ticker.Stop()
   201  				log.Info("All Cilium CRDs have been found and are available")
   202  				return nil
   203  			}
   204  			count++
   205  			if count == 20 {
   206  				count = 0
   207  				log.Infof("Still waiting for Cilium Operator to register the following CRDs: %v", crds.unSynced())
   208  			}
   209  		}
   210  	}
   211  }
   212  
   213  func (s *crdState) add(obj interface{}) {
   214  	if pom := informer.CastInformerEvent[slim_metav1.PartialObjectMetadata](obj); pom != nil {
   215  		s.Lock()
   216  		s.m[CRDResourceName(pom.GetName())] = true
   217  		s.Unlock()
   218  	}
   219  }
   220  
   221  func (s *crdState) remove(obj interface{}) {
   222  	if pom := informer.CastInformerEvent[slim_metav1.PartialObjectMetadata](obj); pom != nil {
   223  		s.Lock()
   224  		s.m[CRDResourceName(pom.GetName())] = false
   225  		s.Unlock()
   226  	}
   227  }
   228  
   229  // isSynced returns whether all the CRDs inside `m` have all been synced,
   230  // meaning all CRDs we care about in Cilium exist in the cluster.
   231  func (s *crdState) isSynced() bool {
   232  	s.Lock()
   233  	defer s.Unlock()
   234  	for _, synced := range s.m {
   235  		if !synced {
   236  			return false
   237  		}
   238  	}
   239  	return true
   240  }
   241  
   242  // unSynced returns a slice containing all CRDs that currently have not been
   243  // synced.
   244  func (s *crdState) unSynced() []string {
   245  	s.Lock()
   246  	defer s.Unlock()
   247  	u := make([]string, 0, len(s.m))
   248  	for crd, synced := range s.m {
   249  		if !synced {
   250  			u = append(u, crd)
   251  		}
   252  	}
   253  	return u
   254  }
   255  
   256  // crdState contains the state of the CRDs inside the cluster.
   257  type crdState struct {
   258  	lock.Mutex
   259  
   260  	// m is a map which maps the CRD name to its synced state in the cluster.
   261  	// True means it exists, false means it doesn't exist.
   262  	m map[string]bool
   263  }
   264  
   265  func newCRDState(crds []string) crdState {
   266  	m := make(map[string]bool, len(crds))
   267  	for _, name := range crds {
   268  		m[name] = false
   269  	}
   270  	return crdState{
   271  		m: m,
   272  	}
   273  }
   274  
   275  // newListWatchFromClient is a copy of the NewListWatchFromClient from the
   276  // "k8s.io/client-go/tools/cache" package, with many alterations made to
   277  // efficiently retrieve Cilium CRDs. Efficient retrieval is important because
   278  // we don't want each agent to fetch the full CRDs across the cluster, because
   279  // they potentially contain large validation schemas.
   280  //
   281  // This function also removes removes unnecessary calls from the upstream
   282  // version that set the namespace and the resource when performing `Get`.
   283  //
   284  //   - If the resource was set, the following error was observed:
   285  //     "customresourcedefinitions.apiextensions.k8s.io
   286  //     "customresourcedefinitions" not found".
   287  //   - If the namespace was set, the following error was observed:
   288  //     "an empty namespace may not be set when a resource name is provided".
   289  //
   290  // The namespace problem can be worked around by using NamespaceIfScoped, but
   291  // it's been omitted entirely here because it's equivalent in functionality.
   292  func newListWatchFromClient(
   293  	c cache.Getter,
   294  	fieldSelector fields.Selector,
   295  ) *cache.ListWatch {
   296  	optionsModifier := func(options *metav1.ListOptions) {
   297  		options.FieldSelector = fieldSelector.String()
   298  	}
   299  
   300  	listFunc := func(options metav1.ListOptions) (runtime.Object, error) {
   301  		optionsModifier(&options)
   302  
   303  		// This lister will retrieve the CRDs as a
   304  		// metav1{,v1beta1}.PartialObjectMetadataList object.
   305  		getter := c.Get()
   306  		// Setting this special header allows us to retrieve the objects the
   307  		// same way that `kubectl get crds` does, except that kubectl retrieves
   308  		// them as a collection inside a metav1{,v1beta1}.Table. Either way, we
   309  		// request the CRDs in a metav1,{v1beta1}.PartialObjectMetadataList
   310  		// object which contains individual metav1.PartialObjectMetadata
   311  		// objects, containing the minimal representation of objects in K8s (in
   312  		// this case a CRD). This matches with what the controller (informer)
   313  		// expects as it wants a list type.
   314  		getter = getter.SetHeader("Accept", pomListHeader)
   315  
   316  		t := &slim_metav1.PartialObjectMetadataList{}
   317  		if err := getter.
   318  			VersionedParams(&options, metav1.ParameterCodec).
   319  			Do(context.TODO()).
   320  			Into(t); err != nil {
   321  			return nil, err
   322  		}
   323  
   324  		return t, nil
   325  	}
   326  	watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
   327  		optionsModifier(&options)
   328  
   329  		getter := c.Get()
   330  		// This watcher will retrieve each CRD that the lister has listed
   331  		// as individual metav1.PartialObjectMetadata because it is
   332  		// requesting the apiserver to return objects as such via the
   333  		// "Accept" header.
   334  		getter = getter.SetHeader("Accept", pomHeader)
   335  
   336  		options.Watch = true
   337  		return getter.
   338  			VersionedParams(&options, metav1.ParameterCodec).
   339  			Watch(context.TODO())
   340  	}
   341  	return &cache.ListWatch{ListFunc: listFunc, WatchFunc: watchFunc}
   342  }
   343  
   344  const (
   345  	pomListHeader = "application/json;as=PartialObjectMetadataList;v=v1;g=meta.k8s.io,application/json;as=PartialObjectMetadataList;v=v1beta1;g=meta.k8s.io,application/json"
   346  	pomHeader     = "application/json;as=PartialObjectMetadata;v=v1;g=meta.k8s.io,application/json;as=PartialObjectMetadata;v=v1beta1;g=meta.k8s.io,application/json"
   347  )
   348  
   349  // Get instantiates a GET request from the K8s REST client to retrieve CRDs. We
   350  // define this getter because it's necessary to use the correct apiextensions
   351  // client (v1 or v1beta1) in order to retrieve the CRDs in a
   352  // backwards-compatible way. This implements the cache.Getter interface.
   353  func (c *crdGetter) Get() *rest.Request {
   354  	return c.api.ApiextensionsV1().
   355  		RESTClient().
   356  		Get().
   357  		Name("customresourcedefinitions")
   358  }
   359  
   360  type crdGetter struct {
   361  	api apiextclientset.Interface
   362  }
   363  
   364  func newCRDGetter(c apiextclientset.Interface) *crdGetter {
   365  	return &crdGetter{api: c}
   366  }