istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/gateway/controller.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 gateway
    16  
    17  import (
    18  	"fmt"
    19  	"sync"
    20  	"time"
    21  
    22  	"go.uber.org/atomic"
    23  	corev1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	klabels "k8s.io/apimachinery/pkg/labels"
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  
    28  	"istio.io/istio/pilot/pkg/credentials"
    29  	"istio.io/istio/pilot/pkg/features"
    30  	"istio.io/istio/pilot/pkg/model"
    31  	"istio.io/istio/pilot/pkg/model/kstatus"
    32  	"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
    33  	"istio.io/istio/pilot/pkg/status"
    34  	"istio.io/istio/pkg/cluster"
    35  	"istio.io/istio/pkg/config"
    36  	"istio.io/istio/pkg/config/labels"
    37  	"istio.io/istio/pkg/config/schema/collection"
    38  	"istio.io/istio/pkg/config/schema/collections"
    39  	"istio.io/istio/pkg/config/schema/gvk"
    40  	"istio.io/istio/pkg/config/schema/gvr"
    41  	"istio.io/istio/pkg/config/schema/kind"
    42  	"istio.io/istio/pkg/kube"
    43  	"istio.io/istio/pkg/kube/controllers"
    44  	"istio.io/istio/pkg/kube/kclient"
    45  	"istio.io/istio/pkg/kube/kubetypes"
    46  	istiolog "istio.io/istio/pkg/log"
    47  	"istio.io/istio/pkg/maps"
    48  	"istio.io/istio/pkg/slices"
    49  	"istio.io/istio/pkg/util/sets"
    50  )
    51  
    52  var log = istiolog.RegisterScope("gateway", "gateway-api controller")
    53  
    54  var errUnsupportedOp = fmt.Errorf("unsupported operation: the gateway config store is a read-only view")
    55  
    56  // Controller defines the controller for the gateway-api. The controller acts a bit different from most.
    57  // Rather than watching the CRs directly, we depend on the existing model.ConfigStoreController which
    58  // already watches all CRs. When there are updates, a new PushContext will be computed, which will eventually
    59  // call Controller.Reconcile(). Once this happens, we will inspect the current state of the world, and transform
    60  // gateway-api types into Istio types (Gateway/VirtualService). Future calls to Get/List will return these
    61  // Istio types. These are not stored in the cluster at all, and are purely internal; they can be seen on /debug/configz.
    62  // During Reconcile(), the status on all gateway-api types is also tracked. Once completed, if the status
    63  // has changed at all, it is queued to asynchronously update the status of the object in Kubernetes.
    64  type Controller struct {
    65  	// client for accessing Kubernetes
    66  	client kube.Client
    67  	// cache provides access to the underlying gateway-configs
    68  	cache model.ConfigStoreController
    69  
    70  	// Gateway-api types reference namespace labels directly, so we need access to these
    71  	namespaces       kclient.Client[*corev1.Namespace]
    72  	namespaceHandler model.EventHandler
    73  
    74  	// Gateway-api types reference secrets directly, so we need access to these
    75  	credentialsController credentials.MulticlusterController
    76  	secretHandler         model.EventHandler
    77  
    78  	// the cluster where the gateway-api controller runs
    79  	cluster cluster.ID
    80  	// domain stores the cluster domain, typically cluster.local
    81  	domain string
    82  
    83  	// state is our computed Istio resources. Access is guarded by stateMu. This is updated from Reconcile().
    84  	state   IstioResources
    85  	stateMu sync.RWMutex
    86  
    87  	// statusController controls the status working queue. Status will only be written if statusEnabled is true, which
    88  	// is only the case when we are the leader.
    89  	statusController *atomic.Pointer[status.Controller]
    90  
    91  	waitForCRD func(class schema.GroupVersionResource, stop <-chan struct{}) bool
    92  }
    93  
    94  var _ model.GatewayController = &Controller{}
    95  
    96  func NewController(
    97  	kc kube.Client,
    98  	c model.ConfigStoreController,
    99  	waitForCRD func(class schema.GroupVersionResource, stop <-chan struct{}) bool,
   100  	credsController credentials.MulticlusterController,
   101  	options controller.Options,
   102  ) *Controller {
   103  	var ctl *status.Controller
   104  
   105  	namespaces := kclient.NewFiltered[*corev1.Namespace](kc, kubetypes.Filter{ObjectFilter: kc.ObjectFilter()})
   106  	gatewayController := &Controller{
   107  		client:                kc,
   108  		cache:                 c,
   109  		namespaces:            namespaces,
   110  		credentialsController: credsController,
   111  		cluster:               options.ClusterID,
   112  		domain:                options.DomainSuffix,
   113  		statusController:      atomic.NewPointer(ctl),
   114  		waitForCRD:            waitForCRD,
   115  	}
   116  
   117  	namespaces.AddEventHandler(controllers.EventHandler[*corev1.Namespace]{
   118  		UpdateFunc: func(oldNs, newNs *corev1.Namespace) {
   119  			if !labels.Instance(oldNs.Labels).Equals(newNs.Labels) {
   120  				gatewayController.namespaceEvent(oldNs, newNs)
   121  			}
   122  		},
   123  	})
   124  
   125  	if credsController != nil {
   126  		credsController.AddSecretHandler(gatewayController.secretEvent)
   127  	}
   128  
   129  	return gatewayController
   130  }
   131  
   132  func (c *Controller) Schemas() collection.Schemas {
   133  	return collection.SchemasFor(
   134  		collections.VirtualService,
   135  		collections.Gateway,
   136  	)
   137  }
   138  
   139  func (c *Controller) Get(typ config.GroupVersionKind, name, namespace string) *config.Config {
   140  	return nil
   141  }
   142  
   143  func (c *Controller) List(typ config.GroupVersionKind, namespace string) []config.Config {
   144  	if typ != gvk.Gateway && typ != gvk.VirtualService {
   145  		return nil
   146  	}
   147  
   148  	c.stateMu.RLock()
   149  	defer c.stateMu.RUnlock()
   150  	switch typ {
   151  	case gvk.Gateway:
   152  		return filterNamespace(c.state.Gateway, namespace)
   153  	case gvk.VirtualService:
   154  		return filterNamespace(c.state.VirtualService, namespace)
   155  	default:
   156  		return nil
   157  	}
   158  }
   159  
   160  func (c *Controller) SetStatusWrite(enabled bool, statusManager *status.Manager) {
   161  	if enabled && features.EnableGatewayAPIStatus && statusManager != nil {
   162  		c.statusController.Store(
   163  			statusManager.CreateGenericController(func(status any, context any) status.GenerationProvider {
   164  				return &gatewayGeneration{context}
   165  			}),
   166  		)
   167  	} else {
   168  		c.statusController.Store(nil)
   169  	}
   170  }
   171  
   172  // Reconcile takes in a current snapshot of the gateway-api configs, and regenerates our internal state.
   173  // Any status updates required will be enqueued as well.
   174  func (c *Controller) Reconcile(ps *model.PushContext) error {
   175  	t0 := time.Now()
   176  	defer func() {
   177  		log.Debugf("reconcile complete in %v", time.Since(t0))
   178  	}()
   179  	gatewayClass := c.cache.List(gvk.GatewayClass, metav1.NamespaceAll)
   180  	gateway := c.cache.List(gvk.KubernetesGateway, metav1.NamespaceAll)
   181  	httpRoute := c.cache.List(gvk.HTTPRoute, metav1.NamespaceAll)
   182  	grpcRoute := c.cache.List(gvk.GRPCRoute, metav1.NamespaceAll)
   183  	tcpRoute := c.cache.List(gvk.TCPRoute, metav1.NamespaceAll)
   184  	tlsRoute := c.cache.List(gvk.TLSRoute, metav1.NamespaceAll)
   185  	referenceGrant := c.cache.List(gvk.ReferenceGrant, metav1.NamespaceAll)
   186  	serviceEntry := c.cache.List(gvk.ServiceEntry, metav1.NamespaceAll) // TODO lazy load only referenced SEs?
   187  
   188  	input := GatewayResources{
   189  		GatewayClass:   deepCopyStatus(gatewayClass),
   190  		Gateway:        deepCopyStatus(gateway),
   191  		HTTPRoute:      deepCopyStatus(httpRoute),
   192  		GRPCRoute:      deepCopyStatus(grpcRoute),
   193  		TCPRoute:       deepCopyStatus(tcpRoute),
   194  		TLSRoute:       deepCopyStatus(tlsRoute),
   195  		ReferenceGrant: referenceGrant,
   196  		ServiceEntry:   serviceEntry,
   197  		Domain:         c.domain,
   198  		Context:        NewGatewayContext(ps, c.cluster),
   199  	}
   200  
   201  	if !input.hasResources() {
   202  		// Early exit for common case of no gateway-api used.
   203  		c.stateMu.Lock()
   204  		defer c.stateMu.Unlock()
   205  		// make sure we clear out the state, to handle the last gateway-api resource being removed
   206  		c.state = IstioResources{}
   207  		return nil
   208  	}
   209  
   210  	nsl := c.namespaces.List("", klabels.Everything())
   211  	namespaces := make(map[string]*corev1.Namespace, len(nsl))
   212  	for _, ns := range nsl {
   213  		namespaces[ns.Name] = ns
   214  	}
   215  	input.Namespaces = namespaces
   216  
   217  	if c.credentialsController != nil {
   218  		credentials, err := c.credentialsController.ForCluster(c.cluster)
   219  		if err != nil {
   220  			return fmt.Errorf("failed to get credentials: %v", err)
   221  		}
   222  		input.Credentials = credentials
   223  	}
   224  
   225  	output := convertResources(input)
   226  
   227  	// Handle all status updates
   228  	c.QueueStatusUpdates(input)
   229  
   230  	c.stateMu.Lock()
   231  	defer c.stateMu.Unlock()
   232  	c.state = output
   233  	return nil
   234  }
   235  
   236  func (c *Controller) QueueStatusUpdates(r GatewayResources) {
   237  	c.handleStatusUpdates(r.GatewayClass)
   238  	c.handleStatusUpdates(r.Gateway)
   239  	c.handleStatusUpdates(r.HTTPRoute)
   240  	c.handleStatusUpdates(r.GRPCRoute)
   241  	c.handleStatusUpdates(r.TCPRoute)
   242  	c.handleStatusUpdates(r.TLSRoute)
   243  }
   244  
   245  func (c *Controller) handleStatusUpdates(configs []config.Config) {
   246  	statusController := c.statusController.Load()
   247  	if statusController == nil {
   248  		return
   249  	}
   250  	for _, cfg := range configs {
   251  		ws := cfg.Status.(*kstatus.WrappedStatus)
   252  		if ws.Dirty {
   253  			res := status.ResourceFromModelConfig(cfg)
   254  			statusController.EnqueueStatusUpdateResource(ws.Unwrap(), res)
   255  		}
   256  	}
   257  }
   258  
   259  func (c *Controller) Create(config config.Config) (revision string, err error) {
   260  	return "", errUnsupportedOp
   261  }
   262  
   263  func (c *Controller) Update(config config.Config) (newRevision string, err error) {
   264  	return "", errUnsupportedOp
   265  }
   266  
   267  func (c *Controller) UpdateStatus(config config.Config) (newRevision string, err error) {
   268  	return "", errUnsupportedOp
   269  }
   270  
   271  func (c *Controller) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) {
   272  	return "", errUnsupportedOp
   273  }
   274  
   275  func (c *Controller) Delete(typ config.GroupVersionKind, name, namespace string, _ *string) error {
   276  	return errUnsupportedOp
   277  }
   278  
   279  func (c *Controller) RegisterEventHandler(typ config.GroupVersionKind, handler model.EventHandler) {
   280  	switch typ {
   281  	case gvk.Namespace:
   282  		c.namespaceHandler = handler
   283  	case gvk.Secret:
   284  		c.secretHandler = handler
   285  	}
   286  	// For all other types, do nothing as c.cache has been registered
   287  }
   288  
   289  func (c *Controller) Run(stop <-chan struct{}) {
   290  	if features.EnableGatewayAPIGatewayClassController {
   291  		go func() {
   292  			if c.waitForCRD(gvr.GatewayClass, stop) {
   293  				gcc := NewClassController(c.client)
   294  				c.client.RunAndWait(stop)
   295  				gcc.Run(stop)
   296  			}
   297  		}()
   298  	}
   299  }
   300  
   301  func (c *Controller) HasSynced() bool {
   302  	return c.cache.HasSynced() && c.namespaces.HasSynced()
   303  }
   304  
   305  func (c *Controller) SecretAllowed(resourceName string, namespace string) bool {
   306  	c.stateMu.RLock()
   307  	defer c.stateMu.RUnlock()
   308  	return c.state.AllowedReferences.SecretAllowed(resourceName, namespace)
   309  }
   310  
   311  // namespaceEvent handles a namespace add/update. Gateway's can select routes by label, so we need to handle
   312  // when the labels change.
   313  // Note: we don't handle delete as a delete would also clean up any relevant gateway-api types which will
   314  // trigger its own event.
   315  func (c *Controller) namespaceEvent(oldNs, newNs *corev1.Namespace) {
   316  	// First, find all the label keys on the old/new namespace. We include NamespaceNameLabel
   317  	// since we have special logic to always allow this on namespace.
   318  	touchedNamespaceLabels := sets.New(NamespaceNameLabel)
   319  	touchedNamespaceLabels.InsertAll(getLabelKeys(oldNs)...)
   320  	touchedNamespaceLabels.InsertAll(getLabelKeys(newNs)...)
   321  
   322  	// Next, we find all keys our Gateways actually reference.
   323  	c.stateMu.RLock()
   324  	intersection := touchedNamespaceLabels.IntersectInPlace(c.state.ReferencedNamespaceKeys)
   325  	c.stateMu.RUnlock()
   326  
   327  	// If there was any overlap, then a relevant namespace label may have changed, and we trigger a
   328  	// push. A more exact check could actually determine if the label selection result actually changed.
   329  	// However, this is a much simpler approach that is likely to scale well enough for now.
   330  	if !intersection.IsEmpty() && c.namespaceHandler != nil {
   331  		log.Debugf("namespace labels changed, triggering namespace handler: %v", intersection.UnsortedList())
   332  		c.namespaceHandler(config.Config{}, config.Config{}, model.EventUpdate)
   333  	}
   334  }
   335  
   336  // getLabelKeys extracts all label keys from a namespace object.
   337  func getLabelKeys(ns *corev1.Namespace) []string {
   338  	if ns == nil {
   339  		return nil
   340  	}
   341  	return maps.Keys(ns.Labels)
   342  }
   343  
   344  func (c *Controller) secretEvent(name, namespace string) {
   345  	var impactedConfigs []model.ConfigKey
   346  	c.stateMu.RLock()
   347  	impactedConfigs = c.state.ResourceReferences[model.ConfigKey{
   348  		Kind:      kind.Secret,
   349  		Namespace: namespace,
   350  		Name:      name,
   351  	}]
   352  	c.stateMu.RUnlock()
   353  	if len(impactedConfigs) > 0 {
   354  		log.Debugf("secret %s/%s changed, triggering secret handler", namespace, name)
   355  		for _, cfg := range impactedConfigs {
   356  			gw := config.Config{
   357  				Meta: config.Meta{
   358  					GroupVersionKind: gvk.KubernetesGateway,
   359  					Namespace:        cfg.Namespace,
   360  					Name:             cfg.Name,
   361  				},
   362  			}
   363  			c.secretHandler(gw, gw, model.EventUpdate)
   364  		}
   365  	}
   366  }
   367  
   368  // deepCopyStatus creates a copy of all configs, with a copy of the status field that we can mutate.
   369  // This allows our functions to call Status.Mutate, and then we can later persist all changes into the
   370  // API server.
   371  func deepCopyStatus(configs []config.Config) []config.Config {
   372  	return slices.Map(configs, func(c config.Config) config.Config {
   373  		return config.Config{
   374  			Meta:   c.Meta,
   375  			Spec:   c.Spec,
   376  			Status: kstatus.Wrap(c.Status),
   377  		}
   378  	})
   379  }
   380  
   381  // filterNamespace allows filtering out configs to only a specific namespace. This allows implementing the
   382  // List call which can specify a specific namespace.
   383  func filterNamespace(cfgs []config.Config, namespace string) []config.Config {
   384  	if namespace == metav1.NamespaceAll {
   385  		return cfgs
   386  	}
   387  	return slices.Filter(cfgs, func(c config.Config) bool {
   388  		return c.Namespace == namespace
   389  	})
   390  }
   391  
   392  // hasResources determines if there are any gateway-api resources created at all.
   393  // If not, we can short circuit all processing to avoid excessive work.
   394  func (kr GatewayResources) hasResources() bool {
   395  	return len(kr.GatewayClass) > 0 ||
   396  		len(kr.Gateway) > 0 ||
   397  		len(kr.HTTPRoute) > 0 ||
   398  		len(kr.GRPCRoute) > 0 ||
   399  		len(kr.TCPRoute) > 0 ||
   400  		len(kr.TLSRoute) > 0 ||
   401  		len(kr.ReferenceGrant) > 0
   402  }