istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/ambient/waypoints.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 // nolint: gocritic 16 package ambient 17 18 import ( 19 "net/netip" 20 "strconv" 21 "strings" 22 23 v1 "k8s.io/api/core/v1" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "sigs.k8s.io/gateway-api/apis/v1beta1" 26 27 "istio.io/istio/pkg/config/constants" 28 "istio.io/istio/pkg/config/schema/gvk" 29 "istio.io/istio/pkg/kube/krt" 30 "istio.io/istio/pkg/log" 31 "istio.io/istio/pkg/ptr" 32 "istio.io/istio/pkg/slices" 33 "istio.io/istio/pkg/workloadapi" 34 ) 35 36 type InboundBinding struct { 37 Port uint32 38 Protocol workloadapi.ApplicationTunnel_Protocol 39 } 40 41 type Waypoint struct { 42 krt.Named 43 44 // Addresses this Waypoint is reachable by. For stock Istio waypoints, this 45 // is is usually the VIP. Tere will always be at least one address in this 46 // list. 47 Addresses []netip.Addr 48 49 // DefaultBinding for an inbound zTunnel to use to connect to a Waypoint it captures. 50 // This is applied to the Workloads that are instances of the current Waypoint. 51 DefaultBinding InboundBinding 52 53 // TrafficType controls whether Service or Workload can reference this 54 // waypoint. Must be one of "all", "service", "workload". 55 TrafficType string 56 57 // ServiceAccounts from instances of the waypoint. 58 // This only handles Pods. If we wish to support non-pod waypoints, we'll 59 // want to index ServiceEntry/WorkloadEntry or possibly allow specifying 60 // the ServiceAccounts directly on a Gateway resource. 61 ServiceAccounts []string 62 } 63 64 // fetchWaypointForInstance attempts to find a Waypoint a given object is an instance of. 65 // TODO should this also lookup waypoints by workload.addresses + workload.services[].vip? 66 // ServiceEntry and WorkloadEntry likely won't have the gateway-name label. 67 func fetchWaypointForInstance(ctx krt.HandlerContext, Waypoints krt.Collection[Waypoint], o metav1.ObjectMeta) *Waypoint { 68 name, namespace := o.GetLabels()[constants.GatewayNameLabel], o.Namespace 69 if name == "" { 70 return nil 71 } 72 return krt.FetchOne[Waypoint](ctx, Waypoints, krt.FilterKey(namespace+"/"+name)) 73 } 74 75 // fetchWaypointForTarget attempts to find the Waypoit that should handle traffic for a given service or workload 76 func fetchWaypointForTarget( 77 ctx krt.HandlerContext, 78 Waypoints krt.Collection[Waypoint], 79 Namespaces krt.Collection[*v1.Namespace], 80 o metav1.ObjectMeta, 81 ) *Waypoint { 82 // namespace to be used when the annotation doesn't include a namespace 83 fallbackNamespace := o.Namespace 84 // try fetching the waypoint defined on the object itself 85 wp, isNone := getUseWaypoint(o, fallbackNamespace) 86 if isNone { 87 // we've got a local override here opting out of waypoint 88 return nil 89 } 90 if wp != nil { 91 // plausible the object has a waypoint defined but that waypoint's underlying gateway is not ready, in this case we'd return nil here even if 92 // the namespace-defined waypoint is ready and would not be nil... is this OK or should we handle that? Could lead to odd behavior when 93 // o was reliant on the namespace waypoint and then get's a use-waypoint label added before that gateway is ready. 94 // goes from having a waypoint to having no waypoint and then eventually gets a waypoint back 95 return krt.FetchOne[Waypoint](ctx, Waypoints, krt.FilterKey(wp.ResourceName())) 96 } 97 98 // try fetching the namespace-defined waypoint 99 namespace := ptr.OrEmpty[*v1.Namespace](krt.FetchOne[*v1.Namespace](ctx, Namespaces, krt.FilterKey(o.Namespace))) 100 // this probably should never be nil. How would o exist in a namespace we know nothing about? maybe edge case of starting the controller or ns delete? 101 if namespace != nil { 102 // toss isNone, we don't need to know /why/ we got nil 103 wpNamespace, _ := getUseWaypoint(namespace.ObjectMeta, fallbackNamespace) 104 if wpNamespace != nil { 105 return krt.FetchOne[Waypoint](ctx, Waypoints, krt.FilterKey(wpNamespace.ResourceName())) 106 } 107 } 108 109 // neither o nor it's namespace has a use-waypoint label 110 return nil 111 } 112 113 func fetchWaypointForService(ctx krt.HandlerContext, Waypoints krt.Collection[Waypoint], 114 Namespaces krt.Collection[*v1.Namespace], o metav1.ObjectMeta, 115 ) *Waypoint { 116 w := fetchWaypointForTarget(ctx, Waypoints, Namespaces, o) 117 if w != nil { 118 if w.TrafficType == constants.ServiceTraffic || w.TrafficType == constants.AllTraffic { 119 return w 120 } 121 // Waypoint does not support Service traffic 122 log.Debugf("Unable to add waypoint %s/%s; traffic type %s not supported for %s/%s", 123 w.Namespace, w.Name, w.TrafficType, o.Namespace, o.Name) 124 } 125 return nil 126 } 127 128 func fetchWaypointForWorkload(ctx krt.HandlerContext, Waypoints krt.Collection[Waypoint], 129 Namespaces krt.Collection[*v1.Namespace], o metav1.ObjectMeta, 130 ) *Waypoint { 131 w := fetchWaypointForTarget(ctx, Waypoints, Namespaces, o) 132 if w != nil { 133 if w.TrafficType == constants.WorkloadTraffic || w.TrafficType == constants.AllTraffic { 134 return w 135 } 136 // Waypoint does not support Workload traffic 137 log.Debugf("Unable to add waypoint %s/%s; traffic type %s not supported for %s/%s", 138 w.Namespace, w.Name, w.TrafficType, o.Namespace, o.Name) 139 } 140 return nil 141 } 142 143 // getUseWaypoint takes objectMeta and a defaultNamespace 144 // it looks for the istio.io/use-waypoint label and parses it 145 // if there is no namespace provided in the label the default namespace will be used 146 // defaultNamespace avoids the need to infer when object meta from a namespace was given 147 func getUseWaypoint(meta metav1.ObjectMeta, defaultNamespace string) (named *krt.Named, isNone bool) { 148 if labelValue, ok := meta.Labels[constants.AmbientUseWaypointLabel]; ok { 149 // NOTE: this means Istio reserves the word "none" in this field with a special meaning 150 // a waypoint named "none" cannot be used and will be ignored 151 // also reserve anything with suffix "/none" to prevent use of "namespace/none" as a work around 152 // ~ is used in other portions of the API, reserve it with special meaning although it's unlikely to be documented 153 if labelValue == "none" || labelValue == "~" || strings.HasSuffix(labelValue, "/none") { 154 return nil, true 155 } 156 namespacedName := strings.Split(labelValue, "/") 157 switch len(namespacedName) { 158 case 1: 159 return &krt.Named{ 160 Name: namespacedName[0], 161 Namespace: defaultNamespace, 162 }, false 163 case 2: 164 return &krt.Named{ 165 Name: namespacedName[1], 166 Namespace: namespacedName[0], 167 }, false 168 default: 169 // malformed label error 170 log.Errorf("%s/%s, has a malformed %s label, value found: %s", meta.GetNamespace(), meta.GetName(), constants.AmbientUseWaypointLabel, labelValue) 171 return nil, false 172 } 173 174 } 175 return nil, false 176 } 177 178 func (w Waypoint) ResourceName() string { 179 return w.GetNamespace() + "/" + w.GetName() 180 } 181 182 func WaypointsCollection( 183 Gateways krt.Collection[*v1beta1.Gateway], 184 GatewayClasses krt.Collection[*v1beta1.GatewayClass], 185 Pods krt.Collection[*v1.Pod], 186 ) krt.Collection[Waypoint] { 187 podsByNamespace := krt.NewNamespaceIndex(Pods) 188 return krt.NewCollection(Gateways, func(ctx krt.HandlerContext, gateway *v1beta1.Gateway) *Waypoint { 189 if len(gateway.Status.Addresses) == 0 { 190 // gateway.Status.Addresses should only be populated once the Waypoint's deployment has at least 1 ready pod, it should never be removed after going ready 191 // ignore Kubernetes Gateways which aren't waypoints 192 return nil 193 } 194 195 instances := krt.Fetch(ctx, Pods, krt.FilterLabel(map[string]string{ 196 constants.GatewayNameLabel: gateway.Name, 197 }), krt.FilterIndex(podsByNamespace, gateway.Namespace)) 198 199 serviceAccounts := slices.Map(instances, func(p *v1.Pod) string { 200 return p.Spec.ServiceAccountName 201 }) 202 203 // default traffic type if neither GatewayClass nor Gateway specify a type 204 trafficType := constants.ServiceTraffic 205 206 gatewayClass := ptr.OrEmpty(krt.FetchOne(ctx, GatewayClasses, krt.FilterKey(string(gateway.Spec.GatewayClassName)))) 207 if gatewayClass == nil { 208 log.Warnf("could not find GatewayClass %s for Gateway %s/%s", gateway.Spec.GatewayClassName, gateway.Namespace, gateway.Name) 209 } else if tt, found := gatewayClass.Labels[constants.AmbientWaypointForTrafficTypeLabel]; found { 210 // Check for a declared traffic type that is allowed to pass through the Waypoint's GatewayClass 211 trafficType = tt 212 } 213 214 // Check for a declared traffic type that is allowed to pass through the Waypoint 215 if tt, found := gateway.Labels[constants.AmbientWaypointForTrafficTypeLabel]; found { 216 trafficType = tt 217 } 218 219 return makeWaypoint(gateway, gatewayClass, serviceAccounts, trafficType) 220 }, krt.WithName("Waypoints")) 221 } 222 223 func makeInboundBinding(gatewayClass *v1beta1.GatewayClass) InboundBinding { 224 if gatewayClass == nil { 225 // zero-value has no dataplane effect 226 return InboundBinding{} 227 } 228 annotation, ok := gatewayClass.Annotations[constants.AmbientWaypointInboundBinding] 229 if !ok { 230 return InboundBinding{} 231 } 232 233 // format is either `protocol` or `protocol/port` 234 parts := strings.Split(annotation, "/") 235 if len(parts) == 0 || len(parts) > 2 { 236 log.Warnf("invalid value %q for %s. Must be of the format \"<protocol>\" or \"<protocol>/<port>\".", annotation, constants.AmbientWaypointInboundBinding) 237 return InboundBinding{} 238 } 239 240 // parse protocol 241 var protocol workloadapi.ApplicationTunnel_Protocol 242 switch parts[0] { 243 case "NONE": 244 protocol = workloadapi.ApplicationTunnel_NONE 245 case "PROXY": 246 protocol = workloadapi.ApplicationTunnel_PROXY 247 default: 248 // Only PROXY is supported for now. 249 log.Warnf("invalid protocol %s for %s. Only NONE or PROXY are supported.", parts[0], constants.AmbientWaypointInboundBinding) 250 return InboundBinding{} 251 } 252 253 // parse port 254 port := uint32(0) 255 if len(parts) == 2 { 256 parsed, err := strconv.ParseUint(parts[1], 10, 32) 257 if err != nil { 258 log.Warnf("invalid port %s for %s.", parts[1], constants.AmbientWaypointInboundBinding) 259 } 260 port = uint32(parsed) 261 } 262 263 return InboundBinding{ 264 Port: port, 265 Protocol: protocol, 266 } 267 } 268 269 func makeWaypoint( 270 gateway *v1beta1.Gateway, 271 gatewayClass *v1beta1.GatewayClass, 272 serviceAccounts []string, 273 trafficType string, 274 ) *Waypoint { 275 return &Waypoint{ 276 Named: krt.NewNamed(gateway), 277 Addresses: getGatewayAddrs(gateway), 278 DefaultBinding: makeInboundBinding(gatewayClass), 279 TrafficType: trafficType, 280 ServiceAccounts: slices.Sort(serviceAccounts), 281 } 282 } 283 284 func getGatewayAddrs(gw *v1beta1.Gateway) []netip.Addr { 285 // Currently, we only look at one address. Probably this should be made more robust 286 ip, err := netip.ParseAddr(gw.Status.Addresses[0].Value) 287 if err == nil { 288 return []netip.Addr{ip} 289 } 290 log.Errorf("Unable to parse IP address in status of %v/%v/%v", gvk.KubernetesGateway, gw.Namespace, gw.Name) 291 return nil 292 }