istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/kclient/crdwatcher.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package kclient 16 17 import ( 18 "fmt" 19 "sync" 20 21 "github.com/Masterminds/semver/v3" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/apimachinery/pkg/runtime/schema" 24 "k8s.io/apimachinery/pkg/types" 25 "sigs.k8s.io/gateway-api/pkg/consts" 26 27 "istio.io/istio/pilot/pkg/features" 28 "istio.io/istio/pkg/config/schema/gvr" 29 "istio.io/istio/pkg/kube" 30 "istio.io/istio/pkg/kube/controllers" 31 "istio.io/istio/pkg/kube/kubetypes" 32 "istio.io/istio/pkg/log" 33 ) 34 35 type crdWatcher struct { 36 crds Informer[*metav1.PartialObjectMetadata] 37 queue controllers.Queue 38 mutex sync.RWMutex 39 callbacks map[string][]func() 40 41 running chan struct{} 42 stop <-chan struct{} 43 } 44 45 func init() { 46 // Unfortunate hack needed to avoid circular imports 47 kube.NewCrdWatcher = newCrdWatcher 48 } 49 50 // newCrdWatcher returns a new CRD watcher controller. 51 func newCrdWatcher(client kube.Client) kubetypes.CrdWatcher { 52 c := &crdWatcher{ 53 running: make(chan struct{}), 54 callbacks: map[string][]func(){}, 55 } 56 57 c.queue = controllers.NewQueue("crd watcher", 58 controllers.WithReconciler(c.Reconcile)) 59 c.crds = NewMetadata(client, gvr.CustomResourceDefinition, Filter{ 60 ObjectFilter: kubetypes.NewStaticObjectFilter(minimumVersionFilter), 61 }) 62 c.crds.AddEventHandler(controllers.ObjectHandler(c.queue.AddObject)) 63 return c 64 } 65 66 var minimumCRDVersions = map[string]*semver.Version{ 67 "grpcroutes.gateway.networking.k8s.io": semver.New(1, 1, 0, "", ""), 68 } 69 70 // minimumVersionFilter filters CRDs that do not meet a minimum "version". 71 // Currently, we use this only for Gateway API CRD's, so we hardcode their versioning scheme. 72 // The problem we are trying to solve is: 73 // * User installs CRDs with Foo v1alpha1 74 // * Istio vNext starts watching Foo at v1 75 // * user upgrades to Istio vNext. It sees Foo exists, and tries to watch v1. This fails. 76 // The user may have opted into using an experimental CRD, but not to experimental usage *in Istio* so this isn't acceptable. 77 func minimumVersionFilter(t any) bool { 78 // Setup a filter 79 crd := t.(*metav1.PartialObjectMetadata) 80 mv, f := minimumCRDVersions[crd.Name] 81 if !f { 82 return true 83 } 84 bv, f := crd.Annotations[consts.BundleVersionAnnotation] 85 if !f { 86 log.Errorf("CRD %v expected to have a %v annotation, but none found; ignoring", crd.Name, consts.BundleVersion) 87 return false 88 } 89 fv, err := semver.NewVersion(bv) 90 if err != nil { 91 log.Errorf("CRD %v version %v invalid; ignoring: %v", crd.Name, bv, err) 92 return false 93 } 94 // Ignore RC tags, etc. We 'round up' those. 95 nv, err := fv.SetPrerelease("") 96 if err != nil { 97 log.Errorf("CRD %v version %v invalid; ignoring: %v", crd.Name, bv, err) 98 return false 99 } 100 fv = &nv 101 if fv.LessThan(mv) { 102 log.Infof("CRD %v version %v is below minimum version %v, ignoring", crd.Name, fv, mv) 103 return false 104 } 105 return true 106 } 107 108 // HasSynced returns whether the underlying cache has synced and the callback has been called at least once. 109 func (c *crdWatcher) HasSynced() bool { 110 return c.queue.HasSynced() 111 } 112 113 // Run starts the controller. This must be called. 114 func (c *crdWatcher) Run(stop <-chan struct{}) { 115 c.mutex.Lock() 116 if c.stop != nil { 117 // Run already called. Because we call this from client.RunAndWait this isn't uncommon 118 c.mutex.Unlock() 119 return 120 } 121 c.stop = stop 122 c.mutex.Unlock() 123 kube.WaitForCacheSync("crd watcher", stop, c.crds.HasSynced) 124 c.queue.Run(stop) 125 c.crds.ShutdownHandlers() 126 } 127 128 // WaitForCRD waits until the request CRD exists, and returns true on success. A false return value 129 // indicates the CRD does not exist but the wait failed or was canceled. 130 // This is useful to conditionally enable controllers based on CRDs being created. 131 func (c *crdWatcher) WaitForCRD(s schema.GroupVersionResource, stop <-chan struct{}) bool { 132 done := make(chan struct{}) 133 if c.KnownOrCallback(s, func(stop <-chan struct{}) { 134 close(done) 135 }) { 136 // Already known 137 return true 138 } 139 select { 140 case <-stop: 141 return false 142 case <-done: 143 return true 144 } 145 } 146 147 // KnownOrCallback returns `true` immediately if the resource is known. 148 // If it is not known, `false` is returned. If the resource is later added, the callback will be triggered. 149 func (c *crdWatcher) KnownOrCallback(s schema.GroupVersionResource, f func(stop <-chan struct{})) bool { 150 c.mutex.Lock() 151 defer c.mutex.Unlock() 152 // If we are already synced, return immediately if the CRD is present. 153 if c.crds.HasSynced() && c.known(s) { 154 // Already known, return early 155 return true 156 } 157 name := fmt.Sprintf("%s.%s", s.Resource, s.Group) 158 c.callbacks[name] = append(c.callbacks[name], func() { 159 if features.EnableUnsafeAssertions && c.stop == nil { 160 log.Fatalf("CRD Watcher callback called without stop set") 161 } 162 // Call the callback 163 f(c.stop) 164 }) 165 return false 166 } 167 168 func (c *crdWatcher) known(s schema.GroupVersionResource) bool { 169 // From the spec: "Its name MUST be in the format <.spec.name>.<.spec.group>." 170 name := fmt.Sprintf("%s.%s", s.Resource, s.Group) 171 return c.crds.Get(name, "") != nil 172 } 173 174 func (c *crdWatcher) Reconcile(key types.NamespacedName) error { 175 c.mutex.Lock() 176 callbacks, f := c.callbacks[key.Name] 177 if !f { 178 c.mutex.Unlock() 179 return nil 180 } 181 // Delete them so we do not run again 182 delete(c.callbacks, key.Name) 183 c.mutex.Unlock() 184 for _, cb := range callbacks { 185 cb() 186 } 187 return nil 188 }