istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/gateway/deploymentcontroller.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  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"strconv"
    22  	"strings"
    23  
    24  	appsv1 "k8s.io/api/apps/v1"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	klabels "k8s.io/apimachinery/pkg/labels"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
    32  	gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
    33  	"sigs.k8s.io/yaml"
    34  
    35  	"istio.io/api/label"
    36  	meshapi "istio.io/api/mesh/v1alpha1"
    37  	"istio.io/istio/pilot/pkg/features"
    38  	"istio.io/istio/pilot/pkg/model"
    39  	"istio.io/istio/pkg/cluster"
    40  	"istio.io/istio/pkg/config/constants"
    41  	"istio.io/istio/pkg/config/protocol"
    42  	"istio.io/istio/pkg/config/schema/gvk"
    43  	"istio.io/istio/pkg/config/schema/gvr"
    44  	common_features "istio.io/istio/pkg/features"
    45  	"istio.io/istio/pkg/kube"
    46  	"istio.io/istio/pkg/kube/controllers"
    47  	"istio.io/istio/pkg/kube/inject"
    48  	"istio.io/istio/pkg/kube/kclient"
    49  	istiolog "istio.io/istio/pkg/log"
    50  	"istio.io/istio/pkg/maps"
    51  	"istio.io/istio/pkg/revisions"
    52  	"istio.io/istio/pkg/test/util/tmpl"
    53  	"istio.io/istio/pkg/test/util/yml"
    54  	"istio.io/istio/pkg/util/sets"
    55  )
    56  
    57  // DeploymentController implements a controller that materializes a Gateway into an in cluster gateway proxy
    58  // to serve requests from. This is implemented with a Deployment and Service today.
    59  // The implementation makes a few non-obvious choices - namely using Server Side Apply from go templates
    60  // and not using controller-runtime.
    61  //
    62  // controller-runtime has a number of constraints that make it inappropriate for usage here, despite this
    63  // seeming to be the bread and butter of the library:
    64  // * It is not readily possible to bring existing Informers, which would require extra watches (#1668)
    65  // * Goroutine leaks (#1655)
    66  // * Excessive API-server calls at startup which have no benefit to us (#1603)
    67  // * Hard to use with SSA (#1669)
    68  // While these can be worked around, at some point it isn't worth the effort.
    69  //
    70  // Server Side Apply with go templates is an odd choice (no one likes YAML templating...) but is one of the few
    71  // remaining options after all others are ruled out.
    72  //   - Merge patch/Update cannot be used. If we always enforce that our object is *exactly* the same as
    73  //     the in-cluster object we will get in endless loops due to other controllers that like to add annotations, etc.
    74  //     If we chose to allow any unknown fields, then we would never be able to remove fields we added, as
    75  //     we cannot tell if we created it or someone else did. SSA fixes these issues
    76  //   - SSA using client-go Apply libraries is almost a good choice, but most third-party clients (Istio, MCS, and gateway-api)
    77  //     do not provide these libraries.
    78  //   - SSA using standard API types doesn't work well either: https://github.com/kubernetes-sigs/controller-runtime/issues/1669
    79  //   - This leaves YAML templates, converted to unstructured types and Applied with the dynamic client.
    80  type DeploymentController struct {
    81  	client         kube.Client
    82  	clusterID      cluster.ID
    83  	env            *model.Environment
    84  	queue          controllers.Queue
    85  	patcher        patcher
    86  	gateways       kclient.Client[*gateway.Gateway]
    87  	gatewayClasses kclient.Client[*gateway.GatewayClass]
    88  
    89  	clients         map[schema.GroupVersionResource]getter
    90  	injectConfig    func() inject.WebhookConfig
    91  	deployments     kclient.Client[*appsv1.Deployment]
    92  	services        kclient.Client[*corev1.Service]
    93  	serviceAccounts kclient.Client[*corev1.ServiceAccount]
    94  	namespaces      kclient.Client[*corev1.Namespace]
    95  	tagWatcher      revisions.TagWatcher
    96  	revision        string
    97  }
    98  
    99  // Patcher is a function that abstracts patching logic. This is largely because client-go fakes do not handle patching
   100  type patcher func(gvr schema.GroupVersionResource, name string, namespace string, data []byte, subresources ...string) error
   101  
   102  // classInfo holds information about a gateway class
   103  type classInfo struct {
   104  	// controller name for this class
   105  	controller string
   106  	// description for this class
   107  	description string
   108  	// The key in the templates to use for this class
   109  	templates string
   110  
   111  	// defaultServiceType sets the default service type if one is not explicit set
   112  	defaultServiceType corev1.ServiceType
   113  
   114  	// disableRouteGeneration, if set, will make it so the controller ignores this class.
   115  	disableRouteGeneration bool
   116  
   117  	// disableNameSuffix, if set, will avoid appending -<class> to names
   118  	disableNameSuffix bool
   119  
   120  	// addressType is the default address type to report
   121  	addressType gateway.AddressType
   122  }
   123  
   124  var classInfos = getClassInfos()
   125  
   126  var builtinClasses = getBuiltinClasses()
   127  
   128  func getBuiltinClasses() map[gateway.ObjectName]gateway.GatewayController {
   129  	res := map[gateway.ObjectName]gateway.GatewayController{
   130  		gateway.ObjectName(features.GatewayAPIDefaultGatewayClass): gateway.GatewayController(features.ManagedGatewayController),
   131  	}
   132  
   133  	if features.MultiNetworkGatewayAPI {
   134  		res[constants.RemoteGatewayClassName] = constants.UnmanagedGatewayController
   135  	}
   136  
   137  	if features.EnableAmbientWaypoints {
   138  		res[constants.WaypointGatewayClassName] = constants.ManagedGatewayMeshController
   139  	}
   140  	return res
   141  }
   142  
   143  func getClassInfos() map[gateway.GatewayController]classInfo {
   144  	m := map[gateway.GatewayController]classInfo{
   145  		gateway.GatewayController(features.ManagedGatewayController): {
   146  			controller:         features.ManagedGatewayController,
   147  			description:        "The default Istio GatewayClass",
   148  			templates:          "kube-gateway",
   149  			defaultServiceType: corev1.ServiceTypeLoadBalancer,
   150  			addressType:        gateway.HostnameAddressType,
   151  		},
   152  	}
   153  
   154  	if features.MultiNetworkGatewayAPI {
   155  		m[constants.UnmanagedGatewayController] = classInfo{
   156  			// This represents a gateway that our control plane cannot discover directly via the API server.
   157  			// We shouldn't generate Istio resources for it. We aren't programming this gateway.
   158  			controller:             constants.UnmanagedGatewayController,
   159  			description:            "Remote to this cluster. Does not deploy or affect configuration.",
   160  			disableRouteGeneration: true,
   161  			addressType:            gateway.HostnameAddressType,
   162  		}
   163  	}
   164  	if features.EnableAmbientWaypoints {
   165  		m[constants.ManagedGatewayMeshController] = classInfo{
   166  			controller:         constants.ManagedGatewayMeshController,
   167  			description:        "The default Istio waypoint GatewayClass",
   168  			templates:          "waypoint",
   169  			disableNameSuffix:  true,
   170  			defaultServiceType: corev1.ServiceTypeClusterIP,
   171  			addressType:        gateway.IPAddressType,
   172  		}
   173  	}
   174  	return m
   175  }
   176  
   177  // NewDeploymentController constructs a DeploymentController and registers required informers.
   178  // The controller will not start until Run() is called.
   179  func NewDeploymentController(client kube.Client, clusterID cluster.ID, env *model.Environment,
   180  	webhookConfig func() inject.WebhookConfig, injectionHandler func(fn func()), tw revisions.TagWatcher, revision string,
   181  ) *DeploymentController {
   182  	filter := kclient.Filter{ObjectFilter: kube.FilterIfEnhancedFilteringEnabled(client)}
   183  	gateways := kclient.NewFiltered[*gateway.Gateway](client, filter)
   184  	gatewayClasses := kclient.New[*gateway.GatewayClass](client)
   185  	dc := &DeploymentController{
   186  		client:    client,
   187  		clusterID: clusterID,
   188  		clients:   map[schema.GroupVersionResource]getter{},
   189  		env:       env,
   190  		patcher: func(gvr schema.GroupVersionResource, name string, namespace string, data []byte, subresources ...string) error {
   191  			c := client.Dynamic().Resource(gvr).Namespace(namespace)
   192  			t := true
   193  			_, err := c.Patch(context.Background(), name, types.ApplyPatchType, data, metav1.PatchOptions{
   194  				Force:        &t,
   195  				FieldManager: features.ManagedGatewayController,
   196  			}, subresources...)
   197  			return err
   198  		},
   199  		gateways:       gateways,
   200  		gatewayClasses: gatewayClasses,
   201  		injectConfig:   webhookConfig,
   202  		tagWatcher:     tw,
   203  		revision:       revision,
   204  	}
   205  	dc.queue = controllers.NewQueue("gateway deployment",
   206  		controllers.WithReconciler(dc.Reconcile),
   207  		controllers.WithMaxAttempts(5))
   208  
   209  	// Set up a handler that will add the parent Gateway object onto the queue.
   210  	// The queue will only handle Gateway objects; if child resources (Service, etc) are updated we re-add
   211  	// the Gateway to the queue and reconcile the state of the world.
   212  	parentHandler := controllers.ObjectHandler(controllers.EnqueueForParentHandler(dc.queue, gvk.KubernetesGateway))
   213  
   214  	dc.services = kclient.NewFiltered[*corev1.Service](client, filter)
   215  	dc.services.AddEventHandler(parentHandler)
   216  	dc.clients[gvr.Service] = NewUntypedWrapper(dc.services)
   217  
   218  	dc.deployments = kclient.NewFiltered[*appsv1.Deployment](client, filter)
   219  	dc.deployments.AddEventHandler(parentHandler)
   220  	dc.clients[gvr.Deployment] = NewUntypedWrapper(dc.deployments)
   221  
   222  	dc.serviceAccounts = kclient.NewFiltered[*corev1.ServiceAccount](client, filter)
   223  	dc.serviceAccounts.AddEventHandler(parentHandler)
   224  	dc.clients[gvr.ServiceAccount] = NewUntypedWrapper(dc.serviceAccounts)
   225  
   226  	dc.namespaces = kclient.NewFiltered[*corev1.Namespace](client, filter)
   227  	dc.namespaces.AddEventHandler(controllers.ObjectHandler(func(o controllers.Object) {
   228  		// TODO: make this more intelligent, checking if something we care about has changed
   229  		// requeue this namespace
   230  		for _, gw := range dc.gateways.List(o.GetName(), klabels.Everything()) {
   231  			dc.queue.AddObject(gw)
   232  		}
   233  	}))
   234  
   235  	gateways.AddEventHandler(controllers.ObjectHandler(dc.queue.AddObject))
   236  	gatewayClasses.AddEventHandler(controllers.ObjectHandler(func(o controllers.Object) {
   237  		for _, g := range dc.gateways.List(metav1.NamespaceAll, klabels.Everything()) {
   238  			if string(g.Spec.GatewayClassName) == o.GetName() {
   239  				dc.queue.AddObject(g)
   240  			}
   241  		}
   242  	}))
   243  
   244  	// On injection template change, requeue all gateways
   245  	injectionHandler(func() {
   246  		for _, gw := range dc.gateways.List(metav1.NamespaceAll, klabels.Everything()) {
   247  			dc.queue.AddObject(gw)
   248  		}
   249  	})
   250  
   251  	dc.tagWatcher.AddHandler(dc.HandleTagChange)
   252  
   253  	return dc
   254  }
   255  
   256  func (d *DeploymentController) Run(stop <-chan struct{}) {
   257  	kube.WaitForCacheSync(
   258  		"deployment controller",
   259  		stop,
   260  		d.namespaces.HasSynced,
   261  		d.deployments.HasSynced,
   262  		d.services.HasSynced,
   263  		d.serviceAccounts.HasSynced,
   264  		d.gateways.HasSynced,
   265  		d.gatewayClasses.HasSynced,
   266  		d.tagWatcher.HasSynced,
   267  	)
   268  	d.queue.Run(stop)
   269  	controllers.ShutdownAll(d.namespaces, d.deployments, d.services, d.serviceAccounts, d.gateways, d.gatewayClasses)
   270  }
   271  
   272  // Reconcile takes in the name of a Gateway and ensures the cluster is in the desired state
   273  func (d *DeploymentController) Reconcile(req types.NamespacedName) error {
   274  	log := log.WithLabels("gateway", req)
   275  
   276  	gw := d.gateways.Get(req.Name, req.Namespace)
   277  	if gw == nil {
   278  		log.Debugf("gateway no longer exists")
   279  		// we'll ignore not-found errors, since they can't be fixed by an immediate
   280  		// requeue (we'll need to wait for a new notification), and we can get them
   281  		// on deleted requests.
   282  		return nil
   283  	}
   284  
   285  	var controller gateway.GatewayController
   286  	if gc := d.gatewayClasses.Get(string(gw.Spec.GatewayClassName), ""); gc != nil {
   287  		controller = gc.Spec.ControllerName
   288  	} else {
   289  		if builtin, f := builtinClasses[gw.Spec.GatewayClassName]; f {
   290  			controller = builtin
   291  		}
   292  	}
   293  	ci, f := classInfos[controller]
   294  	if !f {
   295  		log.Debugf("skipping unknown controller %q", controller)
   296  		return nil
   297  	}
   298  
   299  	// find the tag or revision indicated by the object
   300  	selectedTag, ok := gw.Labels[label.IoIstioRev.Name]
   301  	if !ok {
   302  		ns := d.namespaces.Get(gw.Namespace, "")
   303  		if ns == nil {
   304  			log.Debugf("gateway is not for this revision, skipping")
   305  			return nil
   306  		}
   307  		selectedTag = ns.Labels[label.IoIstioRev.Name]
   308  	}
   309  	myTags := d.tagWatcher.GetMyTags()
   310  	if !myTags.Contains(selectedTag) && !(selectedTag == "" && myTags.Contains("default")) {
   311  		log.Debugf("gateway is not for this revision, skipping")
   312  		return nil
   313  	}
   314  	// TODO: Here we could check if the tag is set and matches no known tags, and handle that if we are default.
   315  
   316  	// Matched class, reconcile it
   317  	return d.configureIstioGateway(log, *gw, ci)
   318  }
   319  
   320  func (d *DeploymentController) configureIstioGateway(log *istiolog.Scope, gw gateway.Gateway, gi classInfo) error {
   321  	// If user explicitly sets addresses, we are assuming they are pointing to an existing deployment.
   322  	// We will not manage it in this case
   323  	if gi.templates == "" {
   324  		log.Debug("skip gateway class without template")
   325  		return nil
   326  	}
   327  	if !IsManaged(&gw.Spec) {
   328  		log.Debug("skip disabled gateway")
   329  		return nil
   330  	}
   331  	existingControllerVersion, overwriteControllerVersion, shouldHandle := ManagedGatewayControllerVersion(gw)
   332  	if !shouldHandle {
   333  		log.Debugf("skipping gateway which is managed by controller version %v", existingControllerVersion)
   334  		return nil
   335  	}
   336  	log.Info("reconciling")
   337  
   338  	var ns *corev1.Namespace
   339  	if d.namespaces != nil {
   340  		ns = d.namespaces.Get(gw.Namespace, "")
   341  	}
   342  	proxyUID, proxyGID := inject.GetProxyIDs(ns)
   343  
   344  	defaultName := getDefaultName(gw.Name, &gw.Spec, gi.disableNameSuffix)
   345  
   346  	serviceType := gi.defaultServiceType
   347  	if o, f := gw.Annotations[serviceTypeOverride]; f {
   348  		serviceType = corev1.ServiceType(o)
   349  	}
   350  
   351  	input := TemplateInput{
   352  		Gateway:        &gw,
   353  		DeploymentName: model.GetOrDefault(gw.Annotations[gatewayNameOverride], defaultName),
   354  		ServiceAccount: model.GetOrDefault(gw.Annotations[gatewaySAOverride], defaultName),
   355  		Ports:          extractServicePorts(gw),
   356  		ClusterID:      d.clusterID.String(),
   357  
   358  		KubeVersion:               kube.GetVersionAsInt(d.client),
   359  		Revision:                  d.revision,
   360  		ServiceType:               serviceType,
   361  		ProxyUID:                  proxyUID,
   362  		ProxyGID:                  proxyGID,
   363  		CompliancePolicy:          common_features.CompliancePolicy,
   364  		InfrastructureLabels:      gw.GetLabels(),
   365  		InfrastructureAnnotations: gw.GetAnnotations(),
   366  	}
   367  
   368  	d.setGatewayNameLabel(&input)
   369  
   370  	// Default to the gateway labels/annotations and overwrite if infrastructure labels/annotations are set
   371  	input.InfrastructureLabels = extractInfrastructureLabels(gw)
   372  	input.InfrastructureAnnotations = extractInfrastructureAnnotations(gw)
   373  	d.setLabelOverrides(gw, input)
   374  
   375  	if overwriteControllerVersion {
   376  		log.Debugf("write controller version, existing=%v", existingControllerVersion)
   377  		if err := d.setGatewayControllerVersion(gw); err != nil {
   378  			return fmt.Errorf("update gateway annotation: %v", err)
   379  		}
   380  	} else {
   381  		log.Debugf("controller version existing=%v, no action needed", existingControllerVersion)
   382  	}
   383  
   384  	rendered, err := d.render(gi.templates, input)
   385  	if err != nil {
   386  		return fmt.Errorf("failed to render template: %v", err)
   387  	}
   388  	for _, t := range rendered {
   389  		if err := d.apply(gi.controller, t); err != nil {
   390  			return fmt.Errorf("apply failed: %v", err)
   391  		}
   392  	}
   393  
   394  	log.Info("gateway updated")
   395  	return nil
   396  }
   397  
   398  func (d *DeploymentController) setLabelOverrides(gw gateway.Gateway, input TemplateInput) {
   399  	// TODO: Codify this API (i.e how to know if a specific gateway is an Istio waypoint gateway)
   400  	isWaypointGateway := strings.Contains(string(gw.Spec.GatewayClassName), "waypoint")
   401  
   402  	var hasAmbientLabel bool
   403  	if _, ok := gw.Labels[constants.DataplaneModeLabel]; ok {
   404  		hasAmbientLabel = true
   405  	}
   406  	if _, ok := input.InfrastructureLabels[constants.DataplaneModeLabel]; ok {
   407  		hasAmbientLabel = true
   408  	}
   409  	// If no ambient redirection label is set explicitly, explicitly disable.
   410  	// TODO this sprays ambient annotations/labels all over EVER gateway resource (serviceaccts, services, etc)
   411  	if features.EnableAmbientWaypoints && !isWaypointGateway && !hasAmbientLabel {
   412  		input.InfrastructureLabels[constants.DataplaneModeLabel] = constants.DataplaneModeNone
   413  	}
   414  
   415  	// Default the network label for waypoints if not explicitly set in gateway's labels
   416  	network := d.injectConfig().Values.Struct().GetGlobal().GetNetwork()
   417  	if _, ok := gw.GetLabels()[label.TopologyNetwork.Name]; !ok && network != "" && isWaypointGateway {
   418  		input.InfrastructureLabels[label.TopologyNetwork.Name] = d.injectConfig().Values.Struct().GetGlobal().GetNetwork()
   419  	}
   420  }
   421  
   422  func extractInfrastructureLabels(gw gateway.Gateway) map[string]string {
   423  	return extractInfrastructureMetadata(gw.Spec.Infrastructure, true, gw)
   424  }
   425  
   426  func extractInfrastructureAnnotations(gw gateway.Gateway) map[string]string {
   427  	return extractInfrastructureMetadata(gw.Spec.Infrastructure, false, gw)
   428  }
   429  
   430  func extractInfrastructureMetadata(gwInfra *gatewayv1.GatewayInfrastructure, isLabel bool, gw gateway.Gateway) map[string]string {
   431  	var field map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue
   432  	if gwInfra != nil && isLabel && gwInfra.Labels != nil {
   433  		field = gwInfra.Labels
   434  	} else if gwInfra != nil && !isLabel && gwInfra.Annotations != nil {
   435  		field = gwInfra.Annotations
   436  	}
   437  	if field != nil {
   438  		infra := make(map[string]string, len(field))
   439  		for k, v := range field {
   440  			if strings.HasPrefix(string(k), "gateway.networking.k8s.io/") {
   441  				continue // ignore this prefix to avoid conflicts
   442  			}
   443  			infra[string(k)] = string(v)
   444  		}
   445  		return infra
   446  	} else if isLabel {
   447  		if gw.GetLabels() == nil {
   448  			return make(map[string]string)
   449  		}
   450  		return maps.Clone(gw.GetLabels())
   451  	}
   452  	if gw.GetAnnotations() == nil {
   453  		return make(map[string]string)
   454  	}
   455  	return maps.Clone(gw.GetAnnotations())
   456  }
   457  
   458  const (
   459  	// ControllerVersionAnnotation is an annotation added to the Gateway by the controller specifying
   460  	// the "controller version". The original intent of this was to work around
   461  	// https://github.com/istio/istio/issues/44164, where we needed to transition from a global owner
   462  	// to a per-revision owner. The newer version number allows forcing ownership, even if the other
   463  	// version was otherwise expected to control the Gateway.
   464  	// The version number has no meaning other than "larger numbers win".
   465  	// Numbers are used to future-proof in case we need to do another migration in the future.
   466  	ControllerVersionAnnotation = "gateway.istio.io/controller-version"
   467  	// ControllerVersion is the current version of our controller logic. Known versions are:
   468  	//
   469  	// * 1.17 and older: version 1 OR no version at all, depending on patch release
   470  	// * 1.18+: version 5
   471  	//
   472  	// 2, 3, and 4 were intentionally skipped to allow for the (unlikely) event we need to insert
   473  	// another version between these
   474  	ControllerVersion = 5
   475  )
   476  
   477  // ManagedGatewayControllerVersion determines the version of the controller managing this Gateway,
   478  // and if we should manage this.
   479  // See ControllerVersionAnnotation for motivations.
   480  func ManagedGatewayControllerVersion(gw gateway.Gateway) (existing string, takeOver bool, manage bool) {
   481  	cur, f := gw.Annotations[ControllerVersionAnnotation]
   482  	if !f {
   483  		// No current owner, we should take it over.
   484  		return "", true, true
   485  	}
   486  	curNum, err := strconv.Atoi(cur)
   487  	if err != nil {
   488  		// We cannot parse it - must be some new schema we don't know about. We should assume we do not manage it.
   489  		// In theory, this should never happen, unless we decide a number was a bad idea in the future.
   490  		return cur, false, false
   491  	}
   492  	if curNum > ControllerVersion {
   493  		// A newer version owns this gateway, let them handle it
   494  		return cur, false, false
   495  	}
   496  	if curNum == ControllerVersion {
   497  		// We already manage this at this version
   498  		// We will manage it, but no need to attempt to apply the version annotation, which could race with newer versions
   499  		return cur, false, true
   500  	}
   501  	// We are either newer or the same version of the last owner - we can take over. We need to actually
   502  	// re-apply the annotation
   503  	return cur, true, true
   504  }
   505  
   506  type derivedInput struct {
   507  	TemplateInput
   508  
   509  	// Inserted from injection config
   510  	ProxyImage  string
   511  	ProxyConfig *meshapi.ProxyConfig
   512  	MeshConfig  *meshapi.MeshConfig
   513  	Values      map[string]any
   514  }
   515  
   516  func (d *DeploymentController) render(templateName string, mi TemplateInput) ([]string, error) {
   517  	cfg := d.injectConfig()
   518  
   519  	template := cfg.Templates[templateName]
   520  	if template == nil {
   521  		return nil, fmt.Errorf("no %q template defined", templateName)
   522  	}
   523  
   524  	labelToMatch := map[string]string{constants.GatewayNameLabel: mi.Name, constants.DeprecatedGatewayNameLabel: mi.Name}
   525  	proxyConfig := d.env.GetProxyConfigOrDefault(mi.Namespace, labelToMatch, nil, cfg.MeshConfig)
   526  	input := derivedInput{
   527  		TemplateInput: mi,
   528  		ProxyImage: inject.ProxyImage(
   529  			cfg.Values.Struct(),
   530  			proxyConfig.GetImage(),
   531  			mi.Annotations,
   532  		),
   533  		ProxyConfig: proxyConfig,
   534  		MeshConfig:  cfg.MeshConfig,
   535  		Values:      cfg.Values.Map(),
   536  	}
   537  	results, err := tmpl.Execute(template, input)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  
   542  	return yml.SplitString(results), nil
   543  }
   544  
   545  func (d *DeploymentController) setGatewayControllerVersion(gws gateway.Gateway) error {
   546  	patch := fmt.Sprintf(`{"apiVersion":"gateway.networking.k8s.io/v1beta1","kind":"Gateway","metadata":{"annotations":{"%s":"%d"}}}`,
   547  		ControllerVersionAnnotation, ControllerVersion)
   548  
   549  	log.Debugf("applying %v", patch)
   550  	return d.patcher(gvr.KubernetesGateway, gws.GetName(), gws.GetNamespace(), []byte(patch))
   551  }
   552  
   553  // apply server-side applies a template to the cluster.
   554  func (d *DeploymentController) apply(controller string, yml string) error {
   555  	data := map[string]any{}
   556  	err := yaml.Unmarshal([]byte(yml), &data)
   557  	if err != nil {
   558  		return err
   559  	}
   560  	us := unstructured.Unstructured{Object: data}
   561  	// set managed-by label
   562  	clabel := strings.ReplaceAll(controller, "/", "-")
   563  	err = unstructured.SetNestedField(us.Object, clabel, "metadata", "labels", constants.ManagedGatewayLabel)
   564  	if err != nil {
   565  		return err
   566  	}
   567  	gvr, err := controllers.UnstructuredToGVR(us)
   568  	if err != nil {
   569  		return err
   570  	}
   571  	j, err := json.Marshal(us.Object)
   572  	if err != nil {
   573  		return err
   574  	}
   575  	canManage, resourceVersion := d.canManage(gvr, us.GetName(), us.GetNamespace())
   576  	if !canManage {
   577  		log.Debugf("skipping %v/%v/%v, already managed", gvr, us.GetName(), us.GetNamespace())
   578  		return nil
   579  	}
   580  	// Ensure our canManage assertion is not stale
   581  	us.SetResourceVersion(resourceVersion)
   582  
   583  	log.Debugf("applying %v", string(j))
   584  	if err := d.patcher(gvr, us.GetName(), us.GetNamespace(), j); err != nil {
   585  		return fmt.Errorf("patch %v/%v/%v: %v", us.GroupVersionKind(), us.GetNamespace(), us.GetName(), err)
   586  	}
   587  	return nil
   588  }
   589  
   590  func (d *DeploymentController) HandleTagChange(newTags sets.String) {
   591  	for _, gw := range d.gateways.List(metav1.NamespaceAll, klabels.Everything()) {
   592  		d.queue.AddObject(gw)
   593  	}
   594  }
   595  
   596  // canManage checks if a resource we are about to write should be managed by us. If the resource already exists
   597  // but does not have the ManagedGatewayLabel, we won't overwrite it.
   598  // This ensures we don't accidentally take over some resource we weren't supposed to, which could cause outages.
   599  // Note K8s doesn't have a perfect way to "conditionally SSA", but its close enough (https://github.com/kubernetes/kubernetes/issues/116156).
   600  func (d *DeploymentController) canManage(gvr schema.GroupVersionResource, name, namespace string) (bool, string) {
   601  	store, f := d.clients[gvr]
   602  	if !f {
   603  		log.Warnf("unknown GVR %v", gvr)
   604  		// Even though we don't know what it is, allow users to put the resource. We won't be able to
   605  		// protect against overwrites though.
   606  		return true, ""
   607  	}
   608  	obj := store.Get(name, namespace)
   609  	if obj == nil {
   610  		// no object, we can manage it
   611  		return true, ""
   612  	}
   613  	_, managed := obj.GetLabels()[constants.ManagedGatewayLabel]
   614  	// If object already exists, we can only manage it if it has the label
   615  	return managed, obj.GetResourceVersion()
   616  }
   617  
   618  // setGatewayNameLabel sets either the new or deprecated gateway name label
   619  // based on the template input
   620  func (d *DeploymentController) setGatewayNameLabel(ti *TemplateInput) {
   621  	ti.GatewayNameLabel = constants.GatewayNameLabel // default to the new gateway name label
   622  	store, f := d.clients[gvr.Deployment]            // Use deployment since those matchlabels are immutable
   623  	if !f {
   624  		log.Warnf("deployment gvr not found in deployment controller clients; defaulting to the new gateway name label")
   625  		return
   626  	}
   627  	dep := store.Get(ti.DeploymentName, ti.Namespace)
   628  	if dep == nil {
   629  		log.Debugf("deployment %s/%s not found in store; using to the new gateway name label", ti.DeploymentName, ti.Namespace)
   630  		return
   631  	}
   632  
   633  	// Base label choice on the deployment's selector
   634  	_, exists := dep.(*appsv1.Deployment).Spec.Selector.MatchLabels[constants.DeprecatedGatewayNameLabel]
   635  	if !exists {
   636  		// The old label doesn't already exist on the deployment; use the new label
   637  		return
   638  	}
   639  
   640  	// The old label exists on the deployment; use the old label
   641  	ti.GatewayNameLabel = constants.DeprecatedGatewayNameLabel
   642  }
   643  
   644  type TemplateInput struct {
   645  	*gateway.Gateway
   646  	DeploymentName            string
   647  	ServiceAccount            string
   648  	Ports                     []corev1.ServicePort
   649  	ServiceType               corev1.ServiceType
   650  	ClusterID                 string
   651  	KubeVersion               int
   652  	Revision                  string
   653  	ProxyUID                  int64
   654  	ProxyGID                  int64
   655  	CompliancePolicy          string
   656  	InfrastructureLabels      map[string]string
   657  	InfrastructureAnnotations map[string]string
   658  	GatewayNameLabel          string
   659  }
   660  
   661  func extractServicePorts(gw gateway.Gateway) []corev1.ServicePort {
   662  	tcp := strings.ToLower(string(protocol.TCP))
   663  	svcPorts := make([]corev1.ServicePort, 0, len(gw.Spec.Listeners)+1)
   664  	svcPorts = append(svcPorts, corev1.ServicePort{
   665  		Name:        "status-port",
   666  		Port:        int32(15021),
   667  		AppProtocol: &tcp,
   668  	})
   669  	portNums := sets.New[int32]()
   670  	for i, l := range gw.Spec.Listeners {
   671  		if portNums.Contains(int32(l.Port)) {
   672  			continue
   673  		}
   674  		portNums.Insert(int32(l.Port))
   675  		name := string(l.Name)
   676  		if name == "" {
   677  			// Should not happen since name is required, but in case an invalid resource gets in...
   678  			name = fmt.Sprintf("%s-%d", strings.ToLower(string(l.Protocol)), i)
   679  		}
   680  		appProtocol := strings.ToLower(string(l.Protocol))
   681  		svcPorts = append(svcPorts, corev1.ServicePort{
   682  			Name:        name,
   683  			Port:        int32(l.Port),
   684  			AppProtocol: &appProtocol,
   685  		})
   686  	}
   687  	return svcPorts
   688  }
   689  
   690  // UntypedWrapper wraps a typed reader to an untyped one, since Go cannot do it automatically.
   691  type UntypedWrapper[T controllers.ComparableObject] struct {
   692  	reader kclient.Reader[T]
   693  }
   694  type getter interface {
   695  	Get(name, namespace string) controllers.Object
   696  }
   697  
   698  func NewUntypedWrapper[T controllers.ComparableObject](c kclient.Client[T]) getter {
   699  	return UntypedWrapper[T]{c}
   700  }
   701  
   702  func (u UntypedWrapper[T]) Get(name, namespace string) controllers.Object {
   703  	// DO NOT return u.reader.Get directly, or we run into issues with https://go.dev/tour/methods/12
   704  	res := u.reader.Get(name, namespace)
   705  	if controllers.IsNil(res) {
   706  		return nil
   707  	}
   708  	return res
   709  }
   710  
   711  var _ getter = UntypedWrapper[*corev1.Service]{}