istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/crdclient/client.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 crdclient provides an implementation of the config store and cache
    16  // using Kubernetes Custom Resources and the informer framework from Kubernetes
    17  //
    18  // This code relies heavily on code generation for performance reasons; to implement the
    19  // Istio store interface, we need to take dynamic inputs. Using the dynamic informers results in poor
    20  // performance, as the cache will store unstructured objects which need to be marshaled on each Get/List call.
    21  // Using istio/client-go directly will cache objects marshaled, allowing us to have cheap Get/List calls,
    22  // at the expense of some code gen.
    23  package crdclient
    24  
    25  import (
    26  	"fmt"
    27  	"sync"
    28  	"time"
    29  
    30  	jsonmerge "github.com/evanphx/json-patch/v5"
    31  	"go.uber.org/atomic"
    32  	"gomodules.xyz/jsonpatch/v2"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	klabels "k8s.io/apimachinery/pkg/labels"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/types"
    37  	"k8s.io/apimachinery/pkg/util/json"
    38  	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"  // import GKE cluster authentication plugin
    39  	_ "k8s.io/client-go/plugin/pkg/client/auth/oidc" // import OIDC cluster authentication plugin, e.g. for Tectonic
    40  
    41  	"istio.io/istio/pilot/pkg/features"
    42  	"istio.io/istio/pilot/pkg/model"
    43  	"istio.io/istio/pkg/config"
    44  	"istio.io/istio/pkg/config/schema/collection"
    45  	"istio.io/istio/pkg/config/schema/collections"
    46  	"istio.io/istio/pkg/config/schema/resource"
    47  	"istio.io/istio/pkg/kube"
    48  	"istio.io/istio/pkg/kube/controllers"
    49  	"istio.io/istio/pkg/kube/kclient"
    50  	"istio.io/istio/pkg/kube/kubetypes"
    51  	"istio.io/istio/pkg/log"
    52  	"istio.io/istio/pkg/maps"
    53  	"istio.io/istio/pkg/queue"
    54  	"istio.io/istio/pkg/slices"
    55  	"istio.io/istio/pkg/util/sets"
    56  )
    57  
    58  var scope = log.RegisterScope("kube", "Kubernetes client messages")
    59  
    60  // Client is a client for Istio CRDs, implementing config store cache
    61  // This is used for CRUD operators on Istio configuration, as well as handling of events on config changes
    62  type Client struct {
    63  	// schemas defines the set of schemas used by this client.
    64  	// Note: this must be a subset of the schemas defined in the codegen
    65  	schemas collection.Schemas
    66  
    67  	// domainSuffix for the config metadata
    68  	domainSuffix string
    69  
    70  	// revision for this control plane instance. We will only read configs that match this revision.
    71  	revision string
    72  
    73  	// kinds keeps track of all cache handlers for known types
    74  	kinds   map[config.GroupVersionKind]kclient.Untyped
    75  	kindsMu sync.RWMutex
    76  	queue   queue.Instance
    77  	// a flag indicates whether this client has been run, it is to prevent run queue twice
    78  	started *atomic.Bool
    79  
    80  	// handlers defines a list of event handlers per-type
    81  	handlers map[config.GroupVersionKind][]model.EventHandler
    82  
    83  	schemasByCRDName map[string]resource.Schema
    84  	client           kube.Client
    85  	logger           *log.Scope
    86  
    87  	// namespacesFilter is only used to initiate filtered informer.
    88  	filtersByGVK map[config.GroupVersionKind]kubetypes.Filter
    89  }
    90  
    91  type Option struct {
    92  	Revision     string
    93  	DomainSuffix string
    94  	Identifier   string
    95  	FiltersByGVK map[config.GroupVersionKind]kubetypes.Filter
    96  }
    97  
    98  var _ model.ConfigStoreController = &Client{}
    99  
   100  func New(client kube.Client, opts Option) *Client {
   101  	schemas := collections.Pilot
   102  	if features.EnableGatewayAPI {
   103  		schemas = collections.PilotGatewayAPI()
   104  	}
   105  	return NewForSchemas(client, opts, schemas)
   106  }
   107  
   108  func NewForSchemas(client kube.Client, opts Option, schemas collection.Schemas) *Client {
   109  	schemasByCRDName := map[string]resource.Schema{}
   110  	for _, s := range schemas.All() {
   111  		// From the spec: "Its name MUST be in the format <.spec.name>.<.spec.group>."
   112  		name := fmt.Sprintf("%s.%s", s.Plural(), s.Group())
   113  		schemasByCRDName[name] = s
   114  	}
   115  	out := &Client{
   116  		domainSuffix:     opts.DomainSuffix,
   117  		schemas:          schemas,
   118  		schemasByCRDName: schemasByCRDName,
   119  		revision:         opts.Revision,
   120  		queue:            queue.NewQueue(1 * time.Second),
   121  		started:          atomic.NewBool(false),
   122  		kinds:            map[config.GroupVersionKind]kclient.Untyped{},
   123  		handlers:         map[config.GroupVersionKind][]model.EventHandler{},
   124  		client:           client,
   125  		logger:           scope.WithLabels("controller", opts.Identifier),
   126  		filtersByGVK:     opts.FiltersByGVK,
   127  	}
   128  
   129  	for _, s := range out.schemas.All() {
   130  		// From the spec: "Its name MUST be in the format <.spec.name>.<.spec.group>."
   131  		name := fmt.Sprintf("%s.%s", s.Plural(), s.Group())
   132  		out.addCRD(name)
   133  	}
   134  
   135  	return out
   136  }
   137  
   138  func (cl *Client) RegisterEventHandler(kind config.GroupVersionKind, handler model.EventHandler) {
   139  	cl.handlers[kind] = append(cl.handlers[kind], handler)
   140  }
   141  
   142  // Run the queue and all informers. Callers should  wait for HasSynced() before depending on results.
   143  func (cl *Client) Run(stop <-chan struct{}) {
   144  	if cl.started.Swap(true) {
   145  		// was already started by other thread
   146  		return
   147  	}
   148  
   149  	t0 := time.Now()
   150  	cl.logger.Infof("Starting Pilot K8S CRD controller")
   151  
   152  	if !kube.WaitForCacheSync("crdclient", stop, cl.informerSynced) {
   153  		cl.logger.Errorf("Failed to sync Pilot K8S CRD controller cache")
   154  		return
   155  	}
   156  	cl.logger.Infof("Pilot K8S CRD controller synced in %v", time.Since(t0))
   157  	cl.queue.Run(stop)
   158  	cl.logger.Infof("controller terminated")
   159  }
   160  
   161  func (cl *Client) informerSynced() bool {
   162  	for gk, ctl := range cl.allKinds() {
   163  		if !ctl.HasSynced() {
   164  			cl.logger.Infof("controller %q is syncing...", gk)
   165  			return false
   166  		}
   167  	}
   168  	return true
   169  }
   170  
   171  func (cl *Client) HasSynced() bool {
   172  	return cl.queue.HasSynced()
   173  }
   174  
   175  // Schemas for the store
   176  func (cl *Client) Schemas() collection.Schemas {
   177  	return cl.schemas
   178  }
   179  
   180  // Get implements store interface
   181  func (cl *Client) Get(typ config.GroupVersionKind, name, namespace string) *config.Config {
   182  	h, f := cl.kind(typ)
   183  	if !f {
   184  		cl.logger.Warnf("unknown type: %s", typ)
   185  		return nil
   186  	}
   187  	obj := h.Get(name, namespace)
   188  	if obj == nil {
   189  		cl.logger.Debugf("couldn't find %s/%s in informer index", namespace, name)
   190  		return nil
   191  	}
   192  
   193  	cfg := TranslateObject(obj, typ, cl.domainSuffix)
   194  	return &cfg
   195  }
   196  
   197  // Create implements store interface
   198  func (cl *Client) Create(cfg config.Config) (string, error) {
   199  	if cfg.Spec == nil {
   200  		return "", fmt.Errorf("nil spec for %v/%v", cfg.Name, cfg.Namespace)
   201  	}
   202  
   203  	meta, err := create(cl.client, cfg, getObjectMetadata(cfg))
   204  	if err != nil {
   205  		return "", err
   206  	}
   207  	return meta.GetResourceVersion(), nil
   208  }
   209  
   210  // Update implements store interface
   211  func (cl *Client) Update(cfg config.Config) (string, error) {
   212  	if cfg.Spec == nil {
   213  		return "", fmt.Errorf("nil spec for %v/%v", cfg.Name, cfg.Namespace)
   214  	}
   215  
   216  	meta, err := update(cl.client, cfg, getObjectMetadata(cfg))
   217  	if err != nil {
   218  		return "", err
   219  	}
   220  	return meta.GetResourceVersion(), nil
   221  }
   222  
   223  func (cl *Client) UpdateStatus(cfg config.Config) (string, error) {
   224  	if cfg.Status == nil {
   225  		return "", fmt.Errorf("nil status for %v/%v on updateStatus()", cfg.Name, cfg.Namespace)
   226  	}
   227  
   228  	meta, err := updateStatus(cl.client, cfg, getObjectMetadata(cfg))
   229  	if err != nil {
   230  		return "", err
   231  	}
   232  	return meta.GetResourceVersion(), nil
   233  }
   234  
   235  // Patch applies only the modifications made in the PatchFunc rather than doing a full replace. Useful to avoid
   236  // read-modify-write conflicts when there are many concurrent-writers to the same resource.
   237  func (cl *Client) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) {
   238  	modified, patchType := patchFn(orig.DeepCopy())
   239  
   240  	meta, err := patch(cl.client, orig, getObjectMetadata(orig), modified, getObjectMetadata(modified), patchType)
   241  	if err != nil {
   242  		return "", err
   243  	}
   244  	return meta.GetResourceVersion(), nil
   245  }
   246  
   247  // Delete implements store interface
   248  // `resourceVersion` must be matched before deletion is carried out. If not possible, a 409 Conflict status will be
   249  func (cl *Client) Delete(typ config.GroupVersionKind, name, namespace string, resourceVersion *string) error {
   250  	return delete(cl.client, typ, name, namespace, resourceVersion)
   251  }
   252  
   253  // List implements store interface
   254  func (cl *Client) List(kind config.GroupVersionKind, namespace string) []config.Config {
   255  	h, f := cl.kind(kind)
   256  	if !f {
   257  		return nil
   258  	}
   259  
   260  	list := h.List(namespace, klabels.Everything())
   261  
   262  	out := make([]config.Config, 0, len(list))
   263  	for _, item := range list {
   264  		cfg := TranslateObject(item, kind, cl.domainSuffix)
   265  		out = append(out, cfg)
   266  	}
   267  
   268  	return out
   269  }
   270  
   271  func (cl *Client) allKinds() map[config.GroupVersionKind]kclient.Untyped {
   272  	cl.kindsMu.RLock()
   273  	defer cl.kindsMu.RUnlock()
   274  	return maps.Clone(cl.kinds)
   275  }
   276  
   277  func (cl *Client) kind(r config.GroupVersionKind) (kclient.Untyped, bool) {
   278  	cl.kindsMu.RLock()
   279  	defer cl.kindsMu.RUnlock()
   280  	ch, ok := cl.kinds[r]
   281  	return ch, ok
   282  }
   283  
   284  func TranslateObject(r runtime.Object, gvk config.GroupVersionKind, domainSuffix string) config.Config {
   285  	translateFunc, f := translationMap[gvk]
   286  	if !f {
   287  		scope.Errorf("unknown type %v", gvk)
   288  		return config.Config{}
   289  	}
   290  	c := translateFunc(r)
   291  	c.Domain = domainSuffix
   292  	return c
   293  }
   294  
   295  func getObjectMetadata(config config.Config) metav1.ObjectMeta {
   296  	return metav1.ObjectMeta{
   297  		Name:            config.Name,
   298  		Namespace:       config.Namespace,
   299  		Labels:          config.Labels,
   300  		Annotations:     config.Annotations,
   301  		ResourceVersion: config.ResourceVersion,
   302  		OwnerReferences: config.OwnerReferences,
   303  		UID:             types.UID(config.UID),
   304  	}
   305  }
   306  
   307  func genPatchBytes(oldRes, modRes runtime.Object, patchType types.PatchType) ([]byte, error) {
   308  	oldJSON, err := json.Marshal(oldRes)
   309  	if err != nil {
   310  		return nil, fmt.Errorf("failed marhsalling original resource: %v", err)
   311  	}
   312  	newJSON, err := json.Marshal(modRes)
   313  	if err != nil {
   314  		return nil, fmt.Errorf("failed marhsalling modified resource: %v", err)
   315  	}
   316  	switch patchType {
   317  	case types.JSONPatchType:
   318  		ops, err := jsonpatch.CreatePatch(oldJSON, newJSON)
   319  		if err != nil {
   320  			return nil, err
   321  		}
   322  		return json.Marshal(ops)
   323  	case types.MergePatchType:
   324  		return jsonmerge.CreateMergePatch(oldJSON, newJSON)
   325  	default:
   326  		return nil, fmt.Errorf("unsupported patch type: %v. must be one of JSONPatchType or MergePatchType", patchType)
   327  	}
   328  }
   329  
   330  func (cl *Client) addCRD(name string) {
   331  	cl.logger.Debugf("adding CRD %q", name)
   332  	s, f := cl.schemasByCRDName[name]
   333  	if !f {
   334  		cl.logger.Debugf("added resource that we are not watching: %v", name)
   335  		return
   336  	}
   337  	resourceGVK := s.GroupVersionKind()
   338  	gvr := s.GroupVersionResource()
   339  
   340  	cl.kindsMu.Lock()
   341  	defer cl.kindsMu.Unlock()
   342  	if _, f := cl.kinds[resourceGVK]; f {
   343  		cl.logger.Debugf("added resource that already exists: %v", resourceGVK)
   344  		return
   345  	}
   346  
   347  	// We need multiple filters:
   348  	// 1. Is it in this revision?
   349  	// 2. Does it match the discovery selector?
   350  	// 3. Does it have a special per-type object filter?
   351  	var extraFilter func(obj any) bool
   352  	if of, f := cl.filtersByGVK[resourceGVK]; f && of.ObjectFilter != nil {
   353  		extraFilter = of.ObjectFilter.Filter
   354  	}
   355  	filter := kubetypes.Filter{ObjectFilter: composeFilters(kube.FilterIfEnhancedFilteringEnabled(cl.client), cl.inRevision, extraFilter)}
   356  
   357  	var kc kclient.Untyped
   358  	if s.IsBuiltin() {
   359  		kc = kclient.NewUntypedInformer(cl.client, gvr, filter)
   360  	} else {
   361  		kc = kclient.NewDelayedInformer[controllers.Object](
   362  			cl.client,
   363  			gvr,
   364  			kubetypes.StandardInformer,
   365  			filter,
   366  		)
   367  	}
   368  
   369  	kind := s.Kind()
   370  	kc.AddEventHandler(controllers.EventHandler[controllers.Object]{
   371  		AddFunc: func(obj controllers.Object) {
   372  			incrementEvent(kind, "add")
   373  			cl.queue.Push(func() error {
   374  				cl.onEvent(resourceGVK, nil, obj, model.EventAdd)
   375  				return nil
   376  			})
   377  		},
   378  		UpdateFunc: func(old, cur controllers.Object) {
   379  			incrementEvent(kind, "update")
   380  			cl.queue.Push(func() error {
   381  				cl.onEvent(resourceGVK, old, cur, model.EventUpdate)
   382  				return nil
   383  			})
   384  		},
   385  		DeleteFunc: func(obj controllers.Object) {
   386  			incrementEvent(kind, "delete")
   387  			cl.queue.Push(func() error {
   388  				cl.onEvent(resourceGVK, nil, obj, model.EventDelete)
   389  				return nil
   390  			})
   391  		},
   392  	})
   393  
   394  	cl.kinds[resourceGVK] = kc
   395  }
   396  
   397  // composedFilter offers a way to join multiple different object filters into a single one
   398  type composedFilter struct {
   399  	// The primary filter, which has a handler. Optional
   400  	filter kubetypes.DynamicObjectFilter
   401  	// Secondary filters (no handler allowed)
   402  	extra []func(obj any) bool
   403  }
   404  
   405  func (f composedFilter) Filter(obj any) bool {
   406  	for _, filter := range f.extra {
   407  		if !filter(obj) {
   408  			return false
   409  		}
   410  	}
   411  	if f.filter != nil {
   412  		return f.filter.Filter(obj)
   413  	}
   414  	return true
   415  }
   416  
   417  func (f composedFilter) AddHandler(fn func(selected, deselected sets.String)) {
   418  	if f.filter != nil {
   419  		f.filter.AddHandler(fn)
   420  	}
   421  }
   422  
   423  func composeFilters(filter kubetypes.DynamicObjectFilter, extra ...func(obj any) bool) kubetypes.DynamicObjectFilter {
   424  	return composedFilter{
   425  		filter: filter,
   426  		extra: slices.FilterInPlace(extra, func(f func(obj any) bool) bool {
   427  			return f != nil
   428  		}),
   429  	}
   430  }
   431  
   432  func (cl *Client) inRevision(obj any) bool {
   433  	return config.LabelsInRevision(obj.(controllers.Object).GetLabels(), cl.revision)
   434  }
   435  
   436  func (cl *Client) onEvent(resourceGVK config.GroupVersionKind, old controllers.Object, curr controllers.Object, event model.Event) {
   437  	currItem := controllers.ExtractObject(curr)
   438  	if currItem == nil {
   439  		return
   440  	}
   441  
   442  	currConfig := TranslateObject(currItem, resourceGVK, cl.domainSuffix)
   443  
   444  	var oldConfig config.Config
   445  	if old != nil {
   446  		oldConfig = TranslateObject(old, resourceGVK, cl.domainSuffix)
   447  	}
   448  
   449  	for _, f := range cl.handlers[resourceGVK] {
   450  		f(oldConfig, currConfig, event)
   451  	}
   452  }