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  }