mirror of
https://github.com/alibaba/higress.git
synced 2026-05-26 21:57:30 +08:00
425 lines
15 KiB
Go
425 lines
15 KiB
Go
// Copyright Istio Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package istio
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
k8s "sigs.k8s.io/gateway-api/apis/v1"
|
|
|
|
higressconstants "github.com/alibaba/higress/v2/pkg/config/constants"
|
|
"istio.io/istio/pilot/pkg/features"
|
|
"istio.io/istio/pilot/pkg/model/kstatus"
|
|
"istio.io/istio/pkg/config/schema/gvk"
|
|
"istio.io/istio/pkg/kube/controllers"
|
|
"istio.io/istio/pkg/maps"
|
|
"istio.io/istio/pkg/slices"
|
|
"istio.io/istio/pkg/util/sets"
|
|
)
|
|
|
|
// RouteParentResult holds the result of a route for a specific parent
|
|
type RouteParentResult struct {
|
|
// OriginalReference contains the original reference
|
|
OriginalReference k8s.ParentReference
|
|
// DeniedReason, if present, indicates why the reference was not valid
|
|
DeniedReason *ParentError
|
|
// RouteError, if present, indicates why the reference was not valid
|
|
RouteError *ConfigError
|
|
// WaypointError, if present, indicates why the reference was does not have a waypoint
|
|
WaypointError *WaypointError
|
|
}
|
|
|
|
func createRouteStatus(
|
|
parentResults []RouteParentResult,
|
|
objectNamespace string,
|
|
generation int64,
|
|
currentParents []k8s.RouteParentStatus,
|
|
) []k8s.RouteParentStatus {
|
|
parents := slices.Clone(currentParents)
|
|
parentIndexes := map[string]int{}
|
|
for idx, p := range parents {
|
|
// Only consider our own
|
|
if p.ControllerName != k8s.GatewayController(higressconstants.ManagedGatewayController) {
|
|
continue
|
|
}
|
|
rs := parentRefString(p.ParentRef, objectNamespace)
|
|
if _, f := parentIndexes[rs]; f {
|
|
log.Warnf("invalid HTTPRoute detected: duplicate parent: %v", rs)
|
|
} else {
|
|
parentIndexes[rs] = idx
|
|
}
|
|
}
|
|
|
|
// Collect all of our unique parent references. There may be multiple when we have a route without section name,
|
|
// but reference a parent with multiple sections.
|
|
// While we process these internally for-each sectionName, in the status we are just supposed to report one merged entry
|
|
seen := map[k8s.ParentReference][]RouteParentResult{}
|
|
seenReasons := sets.New[ParentErrorReason]()
|
|
successCount := map[k8s.ParentReference]int{}
|
|
for _, incoming := range parentResults {
|
|
// We will append it if it is our first occurrence, or the existing one has an error. This means
|
|
// if *any* section has no errors, we will declare Admitted
|
|
if incoming.DeniedReason == nil {
|
|
successCount[incoming.OriginalReference]++
|
|
}
|
|
seen[incoming.OriginalReference] = append(seen[incoming.OriginalReference], incoming)
|
|
if incoming.DeniedReason != nil {
|
|
seenReasons.Insert(incoming.DeniedReason.Reason)
|
|
} else {
|
|
seenReasons.Insert(ParentNoError)
|
|
}
|
|
}
|
|
|
|
const (
|
|
rankParentNoErrors = iota
|
|
rankParentErrorNotAllowed
|
|
rankParentErrorNoHostname
|
|
rankParentErrorParentRefConflict
|
|
rankParentErrorNotAccepted
|
|
)
|
|
|
|
rankParentError := func(result RouteParentResult) int {
|
|
if result.DeniedReason == nil {
|
|
return rankParentNoErrors
|
|
}
|
|
switch result.DeniedReason.Reason {
|
|
case ParentErrorNotAllowed:
|
|
return rankParentErrorNotAllowed
|
|
case ParentErrorNoHostname:
|
|
return rankParentErrorNoHostname
|
|
case ParentErrorParentRefConflict:
|
|
return rankParentErrorParentRefConflict
|
|
case ParentErrorNotAccepted:
|
|
return rankParentErrorNotAccepted
|
|
}
|
|
return rankParentNoErrors
|
|
}
|
|
|
|
// Next we want to collapse these. We need to report 1 type of error, or none.
|
|
report := map[k8s.ParentReference]RouteParentResult{}
|
|
for ref, results := range seen {
|
|
if len(results) == 0 {
|
|
continue
|
|
}
|
|
|
|
toReport := results[0]
|
|
mostSevereRankSeen := rankParentError(toReport)
|
|
|
|
for _, result := range results[1:] {
|
|
resultRank := rankParentError(result)
|
|
// lower number means more severe
|
|
if resultRank < mostSevereRankSeen {
|
|
mostSevereRankSeen = resultRank
|
|
toReport = result
|
|
} else if resultRank == mostSevereRankSeen {
|
|
// join the error messages
|
|
if toReport.DeniedReason == nil {
|
|
toReport.DeniedReason = result.DeniedReason
|
|
} else {
|
|
toReport.DeniedReason.Message += "; " + result.DeniedReason.Message
|
|
}
|
|
}
|
|
}
|
|
|
|
report[ref] = toReport
|
|
}
|
|
|
|
// Now we fill in all the parents we do own
|
|
var toAppend []k8s.RouteParentStatus
|
|
for k, gw := range report {
|
|
msg := "Route was valid"
|
|
if successCount[k] > 1 {
|
|
msg = fmt.Sprintf("Route was valid, bound to %d parents", successCount[k])
|
|
}
|
|
conds := map[string]*condition{
|
|
string(k8s.RouteConditionAccepted): {
|
|
reason: string(k8s.RouteReasonAccepted),
|
|
message: msg,
|
|
},
|
|
string(k8s.RouteConditionResolvedRefs): {
|
|
reason: string(k8s.RouteReasonResolvedRefs),
|
|
message: "All references resolved",
|
|
},
|
|
}
|
|
if gw.RouteError != nil {
|
|
// Currently, the spec is not clear on where errors should be reported. The provided resources are:
|
|
// * Accepted - used to describe errors binding to parents
|
|
// * ResolvedRefs - used to describe errors about binding to objects
|
|
// But no general errors
|
|
// For now, we will treat all general route errors as "Ref" errors.
|
|
conds[string(k8s.RouteConditionResolvedRefs)].error = gw.RouteError
|
|
}
|
|
if gw.DeniedReason != nil {
|
|
conds[string(k8s.RouteConditionAccepted)].error = &ConfigError{
|
|
Reason: ConfigErrorReason(gw.DeniedReason.Reason),
|
|
Message: gw.DeniedReason.Message,
|
|
}
|
|
}
|
|
|
|
// when ambient is enabled, report the waypoints resolved condition
|
|
if features.EnableAmbient {
|
|
cond := &condition{
|
|
reason: string(RouteReasonResolvedWaypoints),
|
|
message: "All waypoints resolved",
|
|
}
|
|
if gw.WaypointError != nil {
|
|
cond.message = gw.WaypointError.Message
|
|
}
|
|
conds[string(RouteConditionResolvedWaypoints)] = cond
|
|
}
|
|
|
|
myRef := parentRefString(gw.OriginalReference, objectNamespace)
|
|
var currentConditions []metav1.Condition
|
|
currentStatus := slices.FindFunc(currentParents, func(s k8s.RouteParentStatus) bool {
|
|
return parentRefString(s.ParentRef, objectNamespace) == myRef &&
|
|
s.ControllerName == k8s.GatewayController(higressconstants.ManagedGatewayController)
|
|
})
|
|
if currentStatus != nil {
|
|
currentConditions = currentStatus.Conditions
|
|
}
|
|
ns := k8s.RouteParentStatus{
|
|
ParentRef: gw.OriginalReference,
|
|
ControllerName: k8s.GatewayController(higressconstants.ManagedGatewayController),
|
|
Conditions: setConditions(generation, currentConditions, conds),
|
|
}
|
|
// Parent ref already exists, insert in the same place
|
|
if idx, f := parentIndexes[myRef]; f {
|
|
parents[idx] = ns
|
|
// Clear it out so we can detect which ones we need to delete later
|
|
delete(parentIndexes, myRef)
|
|
} else {
|
|
// Else queue it up to append to the end. We don't append now since we will want to sort them.
|
|
toAppend = append(toAppend, ns)
|
|
}
|
|
}
|
|
// Ensure output is deterministic.
|
|
// TODO: will we fight over other controllers doing similar (but not identical) ordering?
|
|
sort.SliceStable(toAppend, func(i, j int) bool {
|
|
return parentRefString(toAppend[i].ParentRef, objectNamespace) > parentRefString(toAppend[j].ParentRef, objectNamespace)
|
|
})
|
|
parents = append(parents, toAppend...)
|
|
toDelete := sets.New(maps.Values(parentIndexes)...)
|
|
parents = FilterInPlaceByIndex(parents, func(i int) bool {
|
|
_, f := toDelete[i]
|
|
return !f
|
|
})
|
|
|
|
if parents == nil {
|
|
return []k8s.RouteParentStatus{}
|
|
}
|
|
return parents
|
|
}
|
|
|
|
type ParentErrorReason string
|
|
|
|
const (
|
|
ParentErrorNotAccepted = ParentErrorReason(k8s.RouteReasonNoMatchingParent)
|
|
ParentErrorNotAllowed = ParentErrorReason(k8s.RouteReasonNotAllowedByListeners)
|
|
ParentErrorNoHostname = ParentErrorReason(k8s.RouteReasonNoMatchingListenerHostname)
|
|
ParentErrorParentRefConflict = ParentErrorReason("ParentRefConflict")
|
|
ParentNoError = ParentErrorReason("")
|
|
)
|
|
|
|
type ConfigErrorReason = string
|
|
|
|
const (
|
|
// InvalidDestination indicates an issue with the destination
|
|
InvalidDestination ConfigErrorReason = "InvalidDestination"
|
|
InvalidAddress ConfigErrorReason = ConfigErrorReason(k8s.GatewayReasonUnsupportedAddress)
|
|
// InvalidDestinationPermit indicates a destination was not permitted
|
|
InvalidDestinationPermit ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonRefNotPermitted)
|
|
// InvalidDestinationKind indicates an issue with the destination kind
|
|
InvalidDestinationKind ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonInvalidKind)
|
|
// InvalidDestinationNotFound indicates a destination does not exist
|
|
InvalidDestinationNotFound ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonBackendNotFound)
|
|
// InvalidFilter indicates an issue with the filters
|
|
InvalidFilter ConfigErrorReason = "InvalidFilter"
|
|
// InvalidTLS indicates an issue with TLS settings
|
|
InvalidTLS ConfigErrorReason = ConfigErrorReason(k8s.ListenerReasonInvalidCertificateRef)
|
|
// InvalidListenerRefNotPermitted indicates a listener reference was not permitted
|
|
InvalidListenerRefNotPermitted ConfigErrorReason = ConfigErrorReason(k8s.ListenerReasonRefNotPermitted)
|
|
// InvalidConfiguration indicates a generic error for all other invalid configurations
|
|
InvalidConfiguration ConfigErrorReason = "InvalidConfiguration"
|
|
DeprecateFieldUsage ConfigErrorReason = "DeprecatedField"
|
|
)
|
|
|
|
const (
|
|
// This condition indicates whether a route's parent reference has
|
|
// a waypoint configured by resolving the "istio.io/use-waypoint" label
|
|
// on either the referenced parent or the parent's namespace.
|
|
RouteConditionResolvedWaypoints k8s.RouteConditionType = "ResolvedWaypoints"
|
|
RouteReasonResolvedWaypoints k8s.RouteConditionReason = "ResolvedWaypoints"
|
|
)
|
|
|
|
type WaypointErrorReason string
|
|
|
|
const (
|
|
WaypointErrorReasonMissingLabel = WaypointErrorReason("MissingUseWaypointLabel")
|
|
WaypointErrorMsgMissingLabel = "istio.io/use-waypoint label missing from parent and parent namespace; in ambient mode, route will not be respected"
|
|
WaypointErrorReasonNoMatchingParent = WaypointErrorReason("NoMatchingParent")
|
|
WaypointErrorMsgNoMatchingParent = "parent not found"
|
|
)
|
|
|
|
// ParentError represents that a parent could not be referenced
|
|
type ParentError struct {
|
|
Reason ParentErrorReason
|
|
Message string
|
|
}
|
|
|
|
// ConfigError represents an invalid configuration that will be reported back to the user.
|
|
type ConfigError struct {
|
|
Reason ConfigErrorReason
|
|
Message string
|
|
}
|
|
|
|
type WaypointError struct {
|
|
Reason WaypointErrorReason
|
|
Message string
|
|
}
|
|
|
|
type condition struct {
|
|
// reason defines the reason to report on success. Ignored if error is set
|
|
reason string
|
|
// message defines the message to report on success. Ignored if error is set
|
|
message string
|
|
// status defines the status to report on success. The inverse will be set if error is set
|
|
// If not set, will default to StatusTrue
|
|
status metav1.ConditionStatus
|
|
// error defines an error state; the reason and message will be replaced with that of the error and
|
|
// the status inverted
|
|
error *ConfigError
|
|
// setOnce, if enabled, will only set the condition if it is not yet present or set to this reason
|
|
setOnce string
|
|
}
|
|
|
|
// setConditions sets the existingConditions with the new conditions
|
|
func setConditions(generation int64, existingConditions []metav1.Condition, conditions map[string]*condition) []metav1.Condition {
|
|
// Sort keys for deterministic ordering
|
|
for _, k := range slices.Sort(maps.Keys(conditions)) {
|
|
cond := conditions[k]
|
|
setter := kstatus.UpdateConditionIfChanged
|
|
if cond.setOnce != "" {
|
|
setter = func(conditions []metav1.Condition, condition metav1.Condition) []metav1.Condition {
|
|
return kstatus.CreateCondition(conditions, condition, cond.setOnce)
|
|
}
|
|
}
|
|
// A condition can be "negative polarity" (ex: ListenerInvalid) or "positive polarity" (ex:
|
|
// ListenerValid), so in order to determine the status we should set each `condition` defines its
|
|
// default positive status. When there is an error, we will invert that. Example: If we have
|
|
// condition ListenerInvalid, the status will be set to StatusFalse. If an error is reported, it
|
|
// will be inverted to StatusTrue to indicate listeners are invalid. See
|
|
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
|
|
// for more information
|
|
if cond.error != nil {
|
|
existingConditions = setter(existingConditions, metav1.Condition{
|
|
Type: k,
|
|
Status: kstatus.InvertStatus(cond.status),
|
|
ObservedGeneration: generation,
|
|
LastTransitionTime: metav1.Now(),
|
|
Reason: cond.error.Reason,
|
|
Message: cond.error.Message,
|
|
})
|
|
} else {
|
|
status := cond.status
|
|
if status == "" {
|
|
status = kstatus.StatusTrue
|
|
}
|
|
existingConditions = setter(existingConditions, metav1.Condition{
|
|
Type: k,
|
|
Status: status,
|
|
ObservedGeneration: generation,
|
|
LastTransitionTime: metav1.Now(),
|
|
Reason: cond.reason,
|
|
Message: cond.message,
|
|
})
|
|
}
|
|
}
|
|
return existingConditions
|
|
}
|
|
|
|
func reportListenerCondition(index int, l k8s.Listener, obj controllers.Object,
|
|
statusListeners []k8s.ListenerStatus, conditions map[string]*condition,
|
|
) []k8s.ListenerStatus {
|
|
for index >= len(statusListeners) {
|
|
statusListeners = append(statusListeners, k8s.ListenerStatus{})
|
|
}
|
|
cond := statusListeners[index].Conditions
|
|
supported, valid := generateSupportedKinds(l)
|
|
if !valid {
|
|
conditions[string(k8s.ListenerConditionResolvedRefs)] = &condition{
|
|
reason: string(k8s.ListenerReasonInvalidRouteKinds),
|
|
status: metav1.ConditionFalse,
|
|
message: "Invalid route kinds",
|
|
}
|
|
}
|
|
statusListeners[index] = k8s.ListenerStatus{
|
|
Name: l.Name,
|
|
AttachedRoutes: 0, // this will be reported later
|
|
SupportedKinds: supported,
|
|
Conditions: setConditions(obj.GetGeneration(), cond, conditions),
|
|
}
|
|
return statusListeners
|
|
}
|
|
|
|
func generateSupportedKinds(l k8s.Listener) ([]k8s.RouteGroupKind, bool) {
|
|
supported := []k8s.RouteGroupKind{}
|
|
switch l.Protocol {
|
|
case k8s.HTTPProtocolType, k8s.HTTPSProtocolType:
|
|
// Only terminate allowed, so its always HTTP
|
|
supported = []k8s.RouteGroupKind{
|
|
toRouteKind(gvk.HTTPRoute),
|
|
toRouteKind(gvk.GRPCRoute),
|
|
}
|
|
case k8s.TCPProtocolType:
|
|
supported = []k8s.RouteGroupKind{toRouteKind(gvk.TCPRoute)}
|
|
case k8s.TLSProtocolType:
|
|
if l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == k8s.TLSModePassthrough {
|
|
supported = []k8s.RouteGroupKind{toRouteKind(gvk.TLSRoute)}
|
|
} else {
|
|
supported = []k8s.RouteGroupKind{toRouteKind(gvk.TCPRoute)}
|
|
}
|
|
// UDP route not support
|
|
}
|
|
if l.AllowedRoutes != nil && len(l.AllowedRoutes.Kinds) > 0 {
|
|
// We need to filter down to only ones we actually support
|
|
intersection := []k8s.RouteGroupKind{}
|
|
for _, s := range supported {
|
|
for _, kind := range l.AllowedRoutes.Kinds {
|
|
if routeGroupKindEqual(s, kind) {
|
|
intersection = append(intersection, s)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return intersection, len(intersection) == len(l.AllowedRoutes.Kinds)
|
|
}
|
|
return supported, true
|
|
}
|
|
|
|
func FilterInPlaceByIndex[E any](s []E, keep func(int) bool) []E {
|
|
i := 0
|
|
for j := 0; j < len(s); j++ {
|
|
if keep(j) {
|
|
s[i] = s[j]
|
|
i++
|
|
}
|
|
}
|
|
|
|
clear(s[i:]) // zero/nil out the obsolete elements, for GC
|
|
return s[:i]
|
|
}
|