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 }