istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/gateway/conditions.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  	"fmt"
    19  	"sort"
    20  
    21  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  	k8s "sigs.k8s.io/gateway-api/apis/v1"
    23  
    24  	"istio.io/istio/pilot/pkg/features"
    25  	"istio.io/istio/pilot/pkg/model/kstatus"
    26  	"istio.io/istio/pkg/config"
    27  	"istio.io/istio/pkg/config/schema/gvk"
    28  	"istio.io/istio/pkg/maps"
    29  	"istio.io/istio/pkg/ptr"
    30  	"istio.io/istio/pkg/slices"
    31  	"istio.io/istio/pkg/util/sets"
    32  )
    33  
    34  // RouteParentResult holds the result of a route for a specific parent
    35  type RouteParentResult struct {
    36  	// OriginalReference contains the original reference
    37  	OriginalReference k8s.ParentReference
    38  	// DeniedReason, if present, indicates why the reference was not valid
    39  	DeniedReason *ParentError
    40  	// RouteError, if present, indicates why the reference was not valid
    41  	RouteError *ConfigError
    42  }
    43  
    44  func createRouteStatus(parentResults []RouteParentResult, obj config.Config, currentParents []k8s.RouteParentStatus) []k8s.RouteParentStatus {
    45  	parents := make([]k8s.RouteParentStatus, 0, len(parentResults))
    46  	// Fill in all the gateways that are already present but not owned by us. This is non-trivial as there may be multiple
    47  	// gateway controllers that are exposing their status on the same route. We need to attempt to manage ours properly (including
    48  	// removing gateway references when they are removed), without mangling other Controller's status.
    49  	for _, r := range currentParents {
    50  		if r.ControllerName != k8s.GatewayController(features.ManagedGatewayController) {
    51  			// We don't own this status, so keep it around
    52  			parents = append(parents, r)
    53  		}
    54  	}
    55  	// Collect all of our unique parent references. There may be multiple when we have a route without section name,
    56  	// but reference a parent with multiple sections.
    57  	// While we process these internally for-each sectionName, in the status we are just supposed to report one merged entry
    58  	seen := map[k8s.ParentReference][]RouteParentResult{}
    59  	seenReasons := sets.New[ParentErrorReason]()
    60  	successCount := map[k8s.ParentReference]int{}
    61  	for _, incoming := range parentResults {
    62  		// We will append it if it is our first occurrence, or the existing one has an error. This means
    63  		// if *any* section has no errors, we will declare Admitted
    64  		if incoming.DeniedReason == nil {
    65  			successCount[incoming.OriginalReference]++
    66  		}
    67  		seen[incoming.OriginalReference] = append(seen[incoming.OriginalReference], incoming)
    68  		if incoming.DeniedReason != nil {
    69  			seenReasons.Insert(incoming.DeniedReason.Reason)
    70  		} else {
    71  			seenReasons.Insert(ParentNoError)
    72  		}
    73  	}
    74  	reasonRanking := []ParentErrorReason{
    75  		// No errors is preferred
    76  		ParentNoError,
    77  		// All route level errors
    78  		ParentErrorNotAllowed,
    79  		ParentErrorNoHostname,
    80  		ParentErrorParentRefConflict,
    81  		// Failures to match the Port or SectionName. These are last so that if we bind to 1 listener we
    82  		// just report errors for that 1 listener instead of for all sections we didn't bind to
    83  		ParentErrorNotAccepted,
    84  	}
    85  	// Next we want to collapse these. We need to report 1 type of error, or none.
    86  	report := map[k8s.ParentReference]RouteParentResult{}
    87  	for _, wantReason := range reasonRanking {
    88  		if !seenReasons.Contains(wantReason) {
    89  			continue
    90  		}
    91  		// We found our highest priority ranking, now we need to collapse this into a single message
    92  		for k, refs := range seen {
    93  			for _, ref := range refs {
    94  				reason := ParentNoError
    95  				if ref.DeniedReason != nil {
    96  					reason = ref.DeniedReason.Reason
    97  				}
    98  				if wantReason != reason {
    99  					// Skip this one, it is for a less relevant reason
   100  					continue
   101  				}
   102  				exist, f := report[k]
   103  				if f {
   104  					if ref.DeniedReason != nil {
   105  						if exist.DeniedReason != nil {
   106  							// join the error
   107  							exist.DeniedReason.Message += "; " + ref.DeniedReason.Message
   108  						} else {
   109  							exist.DeniedReason = ref.DeniedReason
   110  						}
   111  					}
   112  				} else {
   113  					exist = ref
   114  				}
   115  				report[k] = exist
   116  			}
   117  		}
   118  		// Once we find the best reason, do not consider any others
   119  		break
   120  	}
   121  
   122  	// Now we fill in all the parents we do own
   123  	for k, gw := range report {
   124  		msg := "Route was valid"
   125  		if successCount[k] > 1 {
   126  			msg = fmt.Sprintf("Route was valid, bound to %d parents", successCount[k])
   127  		}
   128  		conds := map[string]*condition{
   129  			string(k8s.RouteConditionAccepted): {
   130  				reason:  string(k8s.RouteReasonAccepted),
   131  				message: msg,
   132  			},
   133  			string(k8s.RouteConditionResolvedRefs): {
   134  				reason:  string(k8s.RouteReasonResolvedRefs),
   135  				message: "All references resolved",
   136  			},
   137  		}
   138  		if gw.RouteError != nil {
   139  			// Currently, the spec is not clear on where errors should be reported. The provided resources are:
   140  			// * Accepted - used to describe errors binding to parents
   141  			// * ResolvedRefs - used to describe errors about binding to objects
   142  			// But no general errors
   143  			// For now, we will treat all general route errors as "Ref" errors.
   144  			conds[string(k8s.RouteConditionResolvedRefs)].error = gw.RouteError
   145  		}
   146  		if gw.DeniedReason != nil {
   147  			conds[string(k8s.RouteConditionAccepted)].error = &ConfigError{
   148  				Reason:  ConfigErrorReason(gw.DeniedReason.Reason),
   149  				Message: gw.DeniedReason.Message,
   150  			}
   151  		}
   152  
   153  		var currentConditions []metav1.Condition
   154  		currentStatus := slices.FindFunc(currentParents, func(s k8s.RouteParentStatus) bool {
   155  			return parentRefString(s.ParentRef) == parentRefString(gw.OriginalReference) &&
   156  				s.ControllerName == k8s.GatewayController(features.ManagedGatewayController)
   157  		})
   158  		if currentStatus != nil {
   159  			currentConditions = currentStatus.Conditions
   160  		}
   161  		parents = append(parents, k8s.RouteParentStatus{
   162  			ParentRef:      gw.OriginalReference,
   163  			ControllerName: k8s.GatewayController(features.ManagedGatewayController),
   164  			Conditions:     setConditions(obj.Generation, currentConditions, conds),
   165  		})
   166  	}
   167  	// Ensure output is deterministic.
   168  	// TODO: will we fight over other controllers doing similar (but not identical) ordering?
   169  	sort.SliceStable(parents, func(i, j int) bool {
   170  		return parentRefString(parents[i].ParentRef) > parentRefString(parents[j].ParentRef)
   171  	})
   172  	return parents
   173  }
   174  
   175  type ParentErrorReason string
   176  
   177  const (
   178  	ParentErrorNotAccepted       = ParentErrorReason(k8s.RouteReasonNoMatchingParent)
   179  	ParentErrorNotAllowed        = ParentErrorReason(k8s.RouteReasonNotAllowedByListeners)
   180  	ParentErrorNoHostname        = ParentErrorReason(k8s.RouteReasonNoMatchingListenerHostname)
   181  	ParentErrorParentRefConflict = ParentErrorReason("ParentRefConflict")
   182  	ParentNoError                = ParentErrorReason("")
   183  )
   184  
   185  type ConfigErrorReason = string
   186  
   187  const (
   188  	// InvalidRefNotPermitted indicates a route was not permitted
   189  	InvalidRefNotPermitted ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonRefNotPermitted)
   190  	// InvalidDestination indicates an issue with the destination
   191  	InvalidDestination ConfigErrorReason = "InvalidDestination"
   192  	InvalidAddress     ConfigErrorReason = ConfigErrorReason(k8s.GatewayReasonUnsupportedAddress)
   193  	// InvalidDestinationPermit indicates a destination was not permitted
   194  	InvalidDestinationPermit ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonRefNotPermitted)
   195  	// InvalidDestinationKind indicates an issue with the destination kind
   196  	InvalidDestinationKind ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonInvalidKind)
   197  	// InvalidDestinationNotFound indicates a destination does not exist
   198  	InvalidDestinationNotFound ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonBackendNotFound)
   199  	// InvalidParentRef indicates we could not refer to the parent we request
   200  	InvalidParentRef ConfigErrorReason = "InvalidParentReference"
   201  	// InvalidFilter indicates an issue with the filters
   202  	InvalidFilter ConfigErrorReason = "InvalidFilter"
   203  	// InvalidTLS indicates an issue with TLS settings
   204  	InvalidTLS ConfigErrorReason = ConfigErrorReason(k8s.ListenerReasonInvalidCertificateRef)
   205  	// InvalidListenerRefNotPermitted indicates a listener reference was not permitted
   206  	InvalidListenerRefNotPermitted ConfigErrorReason = ConfigErrorReason(k8s.ListenerReasonRefNotPermitted)
   207  	// InvalidConfiguration indicates a generic error for all other invalid configurations
   208  	InvalidConfiguration ConfigErrorReason = "InvalidConfiguration"
   209  	InvalidResources     ConfigErrorReason = ConfigErrorReason(k8s.GatewayReasonNoResources)
   210  	DeprecateFieldUsage                    = "DeprecatedField"
   211  )
   212  
   213  // ParentError represents that a parent could not be referenced
   214  type ParentError struct {
   215  	Reason  ParentErrorReason
   216  	Message string
   217  }
   218  
   219  // ConfigError represents an invalid configuration that will be reported back to the user.
   220  type ConfigError struct {
   221  	Reason  ConfigErrorReason
   222  	Message string
   223  }
   224  
   225  type condition struct {
   226  	// reason defines the reason to report on success. Ignored if error is set
   227  	reason string
   228  	// message defines the message to report on success. Ignored if error is set
   229  	message string
   230  	// status defines the status to report on success. The inverse will be set if error is set
   231  	// If not set, will default to StatusTrue
   232  	status metav1.ConditionStatus
   233  	// error defines an error state; the reason and message will be replaced with that of the error and
   234  	// the status inverted
   235  	error *ConfigError
   236  	// setOnce, if enabled, will only set the condition if it is not yet present or set to this reason
   237  	setOnce string
   238  }
   239  
   240  // setConditions sets the existingConditions with the new conditions
   241  func setConditions(generation int64, existingConditions []metav1.Condition, conditions map[string]*condition) []metav1.Condition {
   242  	// Sort keys for deterministic ordering
   243  	for _, k := range slices.Sort(maps.Keys(conditions)) {
   244  		cond := conditions[k]
   245  		setter := kstatus.UpdateConditionIfChanged
   246  		if cond.setOnce != "" {
   247  			setter = func(conditions []metav1.Condition, condition metav1.Condition) []metav1.Condition {
   248  				return kstatus.CreateCondition(conditions, condition, cond.setOnce)
   249  			}
   250  		}
   251  		// A condition can be "negative polarity" (ex: ListenerInvalid) or "positive polarity" (ex:
   252  		// ListenerValid), so in order to determine the status we should set each `condition` defines its
   253  		// default positive status. When there is an error, we will invert that. Example: If we have
   254  		// condition ListenerInvalid, the status will be set to StatusFalse. If an error is reported, it
   255  		// will be inverted to StatusTrue to indicate listeners are invalid. See
   256  		// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
   257  		// for more information
   258  		if cond.error != nil {
   259  			existingConditions = setter(existingConditions, metav1.Condition{
   260  				Type:               k,
   261  				Status:             kstatus.InvertStatus(cond.status),
   262  				ObservedGeneration: generation,
   263  				LastTransitionTime: metav1.Now(),
   264  				Reason:             cond.error.Reason,
   265  				Message:            cond.error.Message,
   266  			})
   267  		} else {
   268  			status := cond.status
   269  			if status == "" {
   270  				status = kstatus.StatusTrue
   271  			}
   272  			existingConditions = setter(existingConditions, metav1.Condition{
   273  				Type:               k,
   274  				Status:             status,
   275  				ObservedGeneration: generation,
   276  				LastTransitionTime: metav1.Now(),
   277  				Reason:             cond.reason,
   278  				Message:            cond.message,
   279  			})
   280  		}
   281  	}
   282  	return existingConditions
   283  }
   284  
   285  func reportListenerAttachedRoutes(index int, obj config.Config, i int32) {
   286  	obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
   287  		gs := s.(*k8s.GatewayStatus)
   288  		for index >= len(gs.Listeners) {
   289  			gs.Listeners = append(gs.Listeners, k8s.ListenerStatus{})
   290  		}
   291  		status := gs.Listeners[index]
   292  		status.AttachedRoutes = i
   293  		gs.Listeners[index] = status
   294  		return gs
   295  	})
   296  }
   297  
   298  func reportListenerCondition(index int, l k8s.Listener, obj config.Config, conditions map[string]*condition) {
   299  	obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
   300  		gs := s.(*k8s.GatewayStatus)
   301  		for index >= len(gs.Listeners) {
   302  			gs.Listeners = append(gs.Listeners, k8s.ListenerStatus{})
   303  		}
   304  		cond := gs.Listeners[index].Conditions
   305  		supported, valid := generateSupportedKinds(l)
   306  		if !valid {
   307  			conditions[string(k8s.ListenerConditionResolvedRefs)] = &condition{
   308  				reason:  string(k8s.ListenerReasonInvalidRouteKinds),
   309  				status:  metav1.ConditionFalse,
   310  				message: "Invalid route kinds",
   311  			}
   312  		}
   313  		gs.Listeners[index] = k8s.ListenerStatus{
   314  			Name:           l.Name,
   315  			AttachedRoutes: 0, // this will be reported later
   316  			SupportedKinds: supported,
   317  			Conditions:     setConditions(obj.Generation, cond, conditions),
   318  		}
   319  		return gs
   320  	})
   321  }
   322  
   323  func generateSupportedKinds(l k8s.Listener) ([]k8s.RouteGroupKind, bool) {
   324  	supported := []k8s.RouteGroupKind{}
   325  	switch l.Protocol {
   326  	case k8s.HTTPProtocolType, k8s.HTTPSProtocolType:
   327  		// Only terminate allowed, so its always HTTP
   328  		supported = []k8s.RouteGroupKind{
   329  			{Group: (*k8s.Group)(ptr.Of(gvk.HTTPRoute.Group)), Kind: k8s.Kind(gvk.HTTPRoute.Kind)},
   330  			{Group: (*k8s.Group)(ptr.Of(gvk.GRPCRoute.Group)), Kind: k8s.Kind(gvk.GRPCRoute.Kind)},
   331  		}
   332  	case k8s.TCPProtocolType:
   333  		supported = []k8s.RouteGroupKind{{Group: (*k8s.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: k8s.Kind(gvk.TCPRoute.Kind)}}
   334  	case k8s.TLSProtocolType:
   335  		if l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == k8s.TLSModePassthrough {
   336  			supported = []k8s.RouteGroupKind{{Group: (*k8s.Group)(ptr.Of(gvk.TLSRoute.Group)), Kind: k8s.Kind(gvk.TLSRoute.Kind)}}
   337  		} else {
   338  			supported = []k8s.RouteGroupKind{{Group: (*k8s.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: k8s.Kind(gvk.TCPRoute.Kind)}}
   339  		}
   340  		// UDP route note support
   341  	}
   342  	if l.AllowedRoutes != nil && len(l.AllowedRoutes.Kinds) > 0 {
   343  		// We need to filter down to only ones we actually support
   344  		intersection := []k8s.RouteGroupKind{}
   345  		for _, s := range supported {
   346  			for _, kind := range l.AllowedRoutes.Kinds {
   347  				if routeGroupKindEqual(s, kind) {
   348  					intersection = append(intersection, s)
   349  					break
   350  				}
   351  			}
   352  		}
   353  		return intersection, len(intersection) == len(l.AllowedRoutes.Kinds)
   354  	}
   355  	return supported, true
   356  }
   357  
   358  // This and the following function really belongs in some gateway-api lib
   359  func routeGroupKindEqual(rgk1, rgk2 k8s.RouteGroupKind) bool {
   360  	return rgk1.Kind == rgk2.Kind && getGroup(rgk1) == getGroup(rgk2)
   361  }
   362  
   363  func getGroup(rgk k8s.RouteGroupKind) k8s.Group {
   364  	return ptr.OrDefault(rgk.Group, k8s.Group(gvk.KubernetesGateway.Group))
   365  }