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 }