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  }