istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/ingress/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 ingress provides a read-only view of Kubernetes ingress resources
    16  // as an ingress rule configuration type store
    17  package ingress
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"sort"
    23  	"sync"
    24  
    25  	corev1 "k8s.io/api/core/v1"
    26  	knetworking "k8s.io/api/networking/v1"
    27  	klabels "k8s.io/apimachinery/pkg/labels"
    28  	"k8s.io/apimachinery/pkg/types"
    29  
    30  	meshconfig "istio.io/api/mesh/v1alpha1"
    31  	"istio.io/istio/pilot/pkg/model"
    32  	kubecontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
    33  	"istio.io/istio/pkg/config"
    34  	"istio.io/istio/pkg/config/constants"
    35  	"istio.io/istio/pkg/config/mesh"
    36  	"istio.io/istio/pkg/config/schema/collection"
    37  	"istio.io/istio/pkg/config/schema/collections"
    38  	"istio.io/istio/pkg/config/schema/gvk"
    39  	"istio.io/istio/pkg/env"
    40  	"istio.io/istio/pkg/kube"
    41  	"istio.io/istio/pkg/kube/controllers"
    42  	"istio.io/istio/pkg/kube/kclient"
    43  	"istio.io/istio/pkg/util/sets"
    44  )
    45  
    46  // In 1.0, the Gateway is defined in the namespace where the actual controller runs, and needs to be managed by
    47  // user.
    48  // The gateway is named by appending "-istio-autogenerated-k8s-ingress" to the name of the ingress.
    49  //
    50  // Currently the gateway namespace is hardcoded to istio-system (model.IstioIngressNamespace)
    51  //
    52  // VirtualServices are also auto-generated in the model.IstioIngressNamespace.
    53  //
    54  // The sync of Ingress objects to IP is done by status.go
    55  // the 'ingress service' name is used to get the IP of the Service
    56  // If ingress service is empty, it falls back to NodeExternalIP list, selected using the labels.
    57  // This is using 'namespace' of pilot - but seems to be broken (never worked), since it uses Pilot's pod labels
    58  // instead of the ingress labels.
    59  
    60  // Follows mesh.IngressControllerMode setting to enable - OFF|STRICT|DEFAULT.
    61  // STRICT requires "kubernetes.io/ingress.class" == mesh.IngressClass
    62  // DEFAULT allows Ingress without explicit class.
    63  
    64  // In 1.1:
    65  // - K8S_INGRESS_NS - namespace of the Gateway that will act as ingress.
    66  // - labels of the gateway set to "app=ingressgateway" for node_port, service set to 'ingressgateway' (matching default install)
    67  //   If we need more flexibility - we can add it (but likely we'll deprecate ingress support first)
    68  // -
    69  
    70  var schemas = collection.SchemasFor(
    71  	collections.VirtualService,
    72  	collections.Gateway)
    73  
    74  // Control needs RBAC permissions to write to Pods.
    75  
    76  type controller struct {
    77  	meshWatcher  mesh.Holder
    78  	domainSuffix string
    79  
    80  	queue                  controllers.Queue
    81  	virtualServiceHandlers []model.EventHandler
    82  	gatewayHandlers        []model.EventHandler
    83  
    84  	mutex sync.RWMutex
    85  	// processed ingresses
    86  	ingresses map[types.NamespacedName]*knetworking.Ingress
    87  
    88  	classes  kclient.Client[*knetworking.IngressClass]
    89  	ingress  kclient.Client[*knetworking.Ingress]
    90  	services kclient.Client[*corev1.Service]
    91  }
    92  
    93  var IngressNamespace = env.Register("K8S_INGRESS_NS", constants.IstioSystemNamespace,
    94  	"The namespace where ingress controller runs, by default it is istio-system").Get()
    95  
    96  var errUnsupportedOp = errors.New("unsupported operation: the ingress config store is a read-only view")
    97  
    98  // NewController creates a new Kubernetes controller
    99  func NewController(client kube.Client, meshWatcher mesh.Holder,
   100  	options kubecontroller.Options,
   101  ) model.ConfigStoreController {
   102  	ingress := kclient.NewFiltered[*knetworking.Ingress](client, kclient.Filter{ObjectFilter: client.ObjectFilter()})
   103  	classes := kclient.New[*knetworking.IngressClass](client)
   104  	services := kclient.NewFiltered[*corev1.Service](client, kclient.Filter{ObjectFilter: client.ObjectFilter()})
   105  
   106  	c := &controller{
   107  		meshWatcher:  meshWatcher,
   108  		domainSuffix: options.DomainSuffix,
   109  		ingresses:    make(map[types.NamespacedName]*knetworking.Ingress),
   110  		ingress:      ingress,
   111  		classes:      classes,
   112  		services:     services,
   113  	}
   114  	c.queue = controllers.NewQueue("ingress",
   115  		controllers.WithReconciler(c.onEvent),
   116  		controllers.WithMaxAttempts(5))
   117  	c.ingress.AddEventHandler(controllers.ObjectHandler(c.queue.AddObject))
   118  
   119  	// We watch service changes to detect service port number change to trigger
   120  	// re-convert ingress to new-vs.
   121  	c.services.AddEventHandler(controllers.FromEventHandler(func(o controllers.Event) {
   122  		c.onServiceEvent(o)
   123  	}))
   124  
   125  	return c
   126  }
   127  
   128  func (c *controller) Run(stop <-chan struct{}) {
   129  	kube.WaitForCacheSync("ingress", stop, c.ingress.HasSynced, c.services.HasSynced, c.classes.HasSynced)
   130  	c.queue.Run(stop)
   131  	controllers.ShutdownAll(c.ingress, c.services, c.classes)
   132  }
   133  
   134  func (c *controller) shouldProcessIngress(mesh *meshconfig.MeshConfig, i *knetworking.Ingress) bool {
   135  	var class *knetworking.IngressClass
   136  	if i.Spec.IngressClassName != nil {
   137  		c := c.classes.Get(*i.Spec.IngressClassName, "")
   138  		if c == nil {
   139  			return false
   140  		}
   141  		class = c
   142  	}
   143  	return shouldProcessIngressWithClass(mesh, i, class)
   144  }
   145  
   146  // shouldProcessIngressUpdate checks whether we should renotify registered handlers about an update event
   147  func (c *controller) shouldProcessIngressUpdate(ing *knetworking.Ingress) bool {
   148  	// ingress add/update
   149  	shouldProcess := c.shouldProcessIngress(c.meshWatcher.Mesh(), ing)
   150  	item := config.NamespacedName(ing)
   151  	if shouldProcess {
   152  		// record processed ingress
   153  		c.mutex.Lock()
   154  		c.ingresses[item] = ing
   155  		c.mutex.Unlock()
   156  		return true
   157  	}
   158  
   159  	c.mutex.Lock()
   160  	_, preProcessed := c.ingresses[item]
   161  	// previous processed but should not currently, delete it
   162  	if preProcessed && !shouldProcess {
   163  		delete(c.ingresses, item)
   164  	} else {
   165  		c.ingresses[item] = ing
   166  	}
   167  	c.mutex.Unlock()
   168  
   169  	return preProcessed
   170  }
   171  
   172  func (c *controller) onEvent(item types.NamespacedName) error {
   173  	event := model.EventUpdate
   174  	ing := c.ingress.Get(item.Name, item.Namespace)
   175  	if ing == nil {
   176  		event = model.EventDelete
   177  		c.mutex.Lock()
   178  		ing = c.ingresses[item]
   179  		delete(c.ingresses, item)
   180  		c.mutex.Unlock()
   181  		if ing == nil {
   182  			// It was a delete and we didn't have an existing known ingress, no action
   183  			return nil
   184  		}
   185  	}
   186  
   187  	// we should check need process only when event is not delete,
   188  	// if it is delete event, and previously processed, we need to process too.
   189  	if event != model.EventDelete {
   190  		shouldProcess := c.shouldProcessIngressUpdate(ing)
   191  		if !shouldProcess {
   192  			return nil
   193  		}
   194  	}
   195  
   196  	vsmetadata := config.Meta{
   197  		Name:             item.Name + "-" + "virtualservice",
   198  		Namespace:        item.Namespace,
   199  		GroupVersionKind: gvk.VirtualService,
   200  		// Set this label so that we do not compare configs and just push.
   201  		Labels: map[string]string{constants.AlwaysPushLabel: "true"},
   202  	}
   203  	gatewaymetadata := config.Meta{
   204  		Name:             item.Name + "-" + "gateway",
   205  		Namespace:        item.Namespace,
   206  		GroupVersionKind: gvk.Gateway,
   207  		// Set this label so that we do not compare configs and just push.
   208  		Labels: map[string]string{constants.AlwaysPushLabel: "true"},
   209  	}
   210  
   211  	// Trigger updates for Gateway and VirtualService
   212  	// TODO: we could be smarter here and only trigger when real changes were found
   213  	for _, f := range c.virtualServiceHandlers {
   214  		f(config.Config{Meta: vsmetadata}, config.Config{Meta: vsmetadata}, event)
   215  	}
   216  	for _, f := range c.gatewayHandlers {
   217  		f(config.Config{Meta: gatewaymetadata}, config.Config{Meta: gatewaymetadata}, event)
   218  	}
   219  
   220  	return nil
   221  }
   222  
   223  func (c *controller) onServiceEvent(input any) {
   224  	event := input.(controllers.Event)
   225  	curSvc := event.Latest().(*corev1.Service)
   226  
   227  	// This is shortcut. We only care about the port number change if we receive service update event.
   228  	if event.Event == controllers.EventUpdate {
   229  		oldSvc := event.Old.(*corev1.Service)
   230  		oldPorts := extractPorts(oldSvc.Spec.Ports)
   231  		curPorts := extractPorts(curSvc.Spec.Ports)
   232  		// If the ports don't change, we do nothing.
   233  		if oldPorts.Equals(curPorts) {
   234  			return
   235  		}
   236  	}
   237  
   238  	// We care about add, delete and ports changed event of services that are referred
   239  	// by ingress using port name.
   240  	namespacedName := config.NamespacedName(curSvc).String()
   241  	for _, ingress := range c.ingress.List(curSvc.GetNamespace(), klabels.Everything()) {
   242  		referredSvcSet := extractServicesByPortNameType(ingress)
   243  		if referredSvcSet.Contains(namespacedName) {
   244  			c.queue.AddObject(ingress)
   245  		}
   246  	}
   247  }
   248  
   249  func (c *controller) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
   250  	switch kind {
   251  	case gvk.VirtualService:
   252  		c.virtualServiceHandlers = append(c.virtualServiceHandlers, f)
   253  	case gvk.Gateway:
   254  		c.gatewayHandlers = append(c.gatewayHandlers, f)
   255  	}
   256  }
   257  
   258  func (c *controller) HasSynced() bool {
   259  	return c.queue.HasSynced()
   260  }
   261  
   262  func (c *controller) Schemas() collection.Schemas {
   263  	// TODO: are these two config descriptors right?
   264  	return schemas
   265  }
   266  
   267  func (c *controller) Get(typ config.GroupVersionKind, name, namespace string) *config.Config {
   268  	return nil
   269  }
   270  
   271  // sortIngressByCreationTime sorts the list of config objects in ascending order by their creation time (if available).
   272  func sortIngressByCreationTime(ingr []*knetworking.Ingress) []*knetworking.Ingress {
   273  	sort.Slice(ingr, func(i, j int) bool {
   274  		// If creation time is the same, then behavior is nondeterministic. In this case, we can
   275  		// pick an arbitrary but consistent ordering based on name and namespace, which is unique.
   276  		// CreationTimestamp is stored in seconds, so this is not uncommon.
   277  		if ingr[i].CreationTimestamp == ingr[j].CreationTimestamp {
   278  			in := ingr[i].Name + "." + ingr[i].Namespace
   279  			jn := ingr[j].Name + "." + ingr[j].Namespace
   280  			return in < jn
   281  		}
   282  		return ingr[i].CreationTimestamp.Before(&ingr[j].CreationTimestamp)
   283  	})
   284  	return ingr
   285  }
   286  
   287  func (c *controller) List(typ config.GroupVersionKind, namespace string) []config.Config {
   288  	if typ != gvk.Gateway &&
   289  		typ != gvk.VirtualService {
   290  		return nil
   291  	}
   292  
   293  	out := make([]config.Config, 0)
   294  	ingressByHost := map[string]*config.Config{}
   295  	for _, ingress := range sortIngressByCreationTime(c.ingress.List(namespace, klabels.Everything())) {
   296  		process := c.shouldProcessIngress(c.meshWatcher.Mesh(), ingress)
   297  		if !process {
   298  			continue
   299  		}
   300  
   301  		switch typ {
   302  		case gvk.VirtualService:
   303  			ConvertIngressVirtualService(*ingress, c.domainSuffix, ingressByHost, c.services)
   304  		case gvk.Gateway:
   305  			gateways := ConvertIngressV1alpha3(*ingress, c.meshWatcher.Mesh(), c.domainSuffix)
   306  			out = append(out, gateways)
   307  		}
   308  	}
   309  
   310  	if typ == gvk.VirtualService {
   311  		for _, obj := range ingressByHost {
   312  			out = append(out, *obj)
   313  		}
   314  	}
   315  
   316  	return out
   317  }
   318  
   319  // extractServicesByPortNameType extract services that are of port name type in the specified ingress resource.
   320  func extractServicesByPortNameType(ingress *knetworking.Ingress) sets.String {
   321  	services := sets.String{}
   322  	for _, rule := range ingress.Spec.Rules {
   323  		if rule.HTTP == nil {
   324  			continue
   325  		}
   326  
   327  		for _, route := range rule.HTTP.Paths {
   328  			if route.Backend.Service == nil {
   329  				continue
   330  			}
   331  
   332  			if route.Backend.Service.Port.Name != "" {
   333  				services.Insert(types.NamespacedName{
   334  					Namespace: ingress.GetNamespace(),
   335  					Name:      route.Backend.Service.Name,
   336  				}.String())
   337  			}
   338  		}
   339  	}
   340  	return services
   341  }
   342  
   343  func extractPorts(ports []corev1.ServicePort) sets.String {
   344  	result := sets.String{}
   345  	for _, port := range ports {
   346  		// the format is port number|port name.
   347  		result.Insert(fmt.Sprintf("%d|%s", port.Port, port.Name))
   348  	}
   349  	return result
   350  }
   351  
   352  func (c *controller) Create(_ config.Config) (string, error) {
   353  	return "", errUnsupportedOp
   354  }
   355  
   356  func (c *controller) Update(_ config.Config) (string, error) {
   357  	return "", errUnsupportedOp
   358  }
   359  
   360  func (c *controller) UpdateStatus(config.Config) (string, error) {
   361  	return "", errUnsupportedOp
   362  }
   363  
   364  func (c *controller) Patch(_ config.Config, _ config.PatchFunc) (string, error) {
   365  	return "", errUnsupportedOp
   366  }
   367  
   368  func (c *controller) Delete(_ config.GroupVersionKind, _, _ string, _ *string) error {
   369  	return errUnsupportedOp
   370  }