1
// Copyright Istio Authors
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
7
// http://www.apache.org/licenses/LICENSE-2.0
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.
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"
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"
52
var log = istiolog.RegisterScope("gateway", "gateway-api controller")
54
var errUnsupportedOp = fmt.Errorf("unsupported operation: the gateway config store is a read-only view")
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
67
// cache provides access to the underlying gateway-configs
68
cache model.ConfigStoreController
70
// Gateway-api types reference namespace labels directly, so we need access to these
71
namespaces kclient.Client[*corev1.Namespace]
72
namespaceHandler model.EventHandler
74
// Gateway-api types reference secrets directly, so we need access to these
75
credentialsController credentials.MulticlusterController
76
secretHandler model.EventHandler
78
// the cluster where the gateway-api controller runs
80
// domain stores the cluster domain, typically cluster.local
83
// state is our computed Istio resources. Access is guarded by stateMu. This is updated from Reconcile().
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]
91
waitForCRD func(class schema.GroupVersionResource, stop <-chan struct{}) bool
94
var _ model.GatewayController = &Controller{}
98
c model.ConfigStoreController,
99
waitForCRD func(class schema.GroupVersionResource, stop <-chan struct{}) bool,
100
credsController credentials.MulticlusterController,
101
options controller.Options,
103
var ctl *status.Controller
105
namespaces := kclient.NewFiltered[*corev1.Namespace](kc, kubetypes.Filter{ObjectFilter: kc.ObjectFilter()})
106
gatewayController := &Controller{
109
namespaces: namespaces,
110
credentialsController: credsController,
111
cluster: options.ClusterID,
112
domain: options.DomainSuffix,
113
statusController: atomic.NewPointer(ctl),
114
waitForCRD: waitForCRD,
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)
125
if credsController != nil {
126
credsController.AddSecretHandler(gatewayController.secretEvent)
129
return gatewayController
132
func (c *Controller) Schemas() collection.Schemas {
133
return collection.SchemasFor(
134
collections.VirtualService,
139
func (c *Controller) Get(typ config.GroupVersionKind, name, namespace string) *config.Config {
143
func (c *Controller) List(typ config.GroupVersionKind, namespace string) []config.Config {
144
if typ != gvk.Gateway && typ != gvk.VirtualService {
149
defer c.stateMu.RUnlock()
152
return filterNamespace(c.state.Gateway, namespace)
153
case gvk.VirtualService:
154
return filterNamespace(c.state.VirtualService, namespace)
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}
168
c.statusController.Store(nil)
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 {
177
log.Debugf("reconcile complete in %v", time.Since(t0))
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?
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,
198
Context: NewGatewayContext(ps, c.cluster),
201
if !input.hasResources() {
202
// Early exit for common case of no gateway-api used.
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{}
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
215
input.Namespaces = namespaces
217
if c.credentialsController != nil {
218
credentials, err := c.credentialsController.ForCluster(c.cluster)
220
return fmt.Errorf("failed to get credentials: %v", err)
222
input.Credentials = credentials
225
output := convertResources(input)
227
// Handle all status updates
228
c.QueueStatusUpdates(input)
231
defer c.stateMu.Unlock()
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)
245
func (c *Controller) handleStatusUpdates(configs []config.Config) {
246
statusController := c.statusController.Load()
247
if statusController == nil {
250
for _, cfg := range configs {
251
ws := cfg.Status.(*kstatus.WrappedStatus)
253
res := status.ResourceFromModelConfig(cfg)
254
statusController.EnqueueStatusUpdateResource(ws.Unwrap(), res)
259
func (c *Controller) Create(config config.Config) (revision string, err error) {
260
return "", errUnsupportedOp
263
func (c *Controller) Update(config config.Config) (newRevision string, err error) {
264
return "", errUnsupportedOp
267
func (c *Controller) UpdateStatus(config config.Config) (newRevision string, err error) {
268
return "", errUnsupportedOp
271
func (c *Controller) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) {
272
return "", errUnsupportedOp
275
func (c *Controller) Delete(typ config.GroupVersionKind, name, namespace string, _ *string) error {
276
return errUnsupportedOp
279
func (c *Controller) RegisterEventHandler(typ config.GroupVersionKind, handler model.EventHandler) {
282
c.namespaceHandler = handler
284
c.secretHandler = handler
286
// For all other types, do nothing as c.cache has been registered
289
func (c *Controller) Run(stop <-chan struct{}) {
290
if features.EnableGatewayAPIGatewayClassController {
292
if c.waitForCRD(gvr.GatewayClass, stop) {
293
gcc := NewClassController(c.client)
294
c.client.RunAndWait(stop)
301
func (c *Controller) HasSynced() bool {
302
return c.cache.HasSynced() && c.namespaces.HasSynced()
305
func (c *Controller) SecretAllowed(resourceName string, namespace string) bool {
307
defer c.stateMu.RUnlock()
308
return c.state.AllowedReferences.SecretAllowed(resourceName, namespace)
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)...)
322
// Next, we find all keys our Gateways actually reference.
324
intersection := touchedNamespaceLabels.Intersection(c.state.ReferencedNamespaceKeys)
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)
336
// getLabelKeys extracts all label keys from a namespace object.
337
func getLabelKeys(ns *corev1.Namespace) []string {
341
return maps.Keys(ns.Labels)
344
func (c *Controller) secretEvent(name, namespace string) {
345
var impactedConfigs []model.ConfigKey
347
impactedConfigs = c.state.ResourceReferences[model.ConfigKey{
349
Namespace: namespace,
353
if len(impactedConfigs) > 0 {
354
log.Debugf("secret %s/%s changed, triggering secret handler", namespace, name)
355
for _, cfg := range impactedConfigs {
358
GroupVersionKind: gvk.KubernetesGateway,
359
Namespace: cfg.Namespace,
363
c.secretHandler(gw, gw, model.EventUpdate)
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
371
func deepCopyStatus(configs []config.Config) []config.Config {
372
return slices.Map(configs, func(c config.Config) config.Config {
373
return config.Config{
376
Status: kstatus.Wrap(c.Status),
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 {
387
return slices.Filter(cfgs, func(c config.Config) bool {
388
return c.Namespace == namespace
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