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]{}