// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package api import ( "fmt" "time" "golang.org/x/exp/slices" ) // ResourceReference is a reference to a ConfigEntry // with an optional reference to a subsection of that ConfigEntry // that can be specified as SectionName type ResourceReference struct { // Kind is the kind of ConfigEntry that this resource refers to. Kind string // Name is the identifier for the ConfigEntry this resource refers to. Name string // SectionName is a generic subresource identifier that specifies // a subset of the ConfigEntry to which this reference applies. Usage // of this field should be up to the controller that leverages it. If // unused, this should be blank. SectionName string // Partition is the partition the config entry is associated with. // Partitioning is a Consul Enterprise feature. Partition string `json:",omitempty"` // Namespace is the namespace the config entry is associated with. // Namespacing is a Consul Enterprise feature. Namespace string `json:",omitempty"` } // ConfigEntryStatus is used for propagating back asynchronously calculated // messages from control loops to a user type ConfigEntryStatus struct { // Conditions is the set of condition objects associated with // a ConfigEntry status. Conditions []Condition } // Condition is used for a single message and state associated // with an object. For example, a ConfigEntry that references // multiple other resources may have different statuses with // respect to each of those resources. type Condition struct { // Type is a value from a bounded set of types that an object might have Type string // Status is a value from a bounded set of statuses that an object might have Status ConditionStatus // Reason is a value from a bounded set of reasons for a given status Reason string // Message is a message that gives more detailed information about // why a Condition has a given status and reason Message string // Resource is an optional reference to a resource for which this // condition applies Resource *ResourceReference // LastTransitionTime is the time at which this Condition was created LastTransitionTime *time.Time } type ( ConditionStatus string ) const ( ConditionStatusTrue ConditionStatus = "True" ConditionStatusFalse ConditionStatus = "False" ConditionStatusUnknown ConditionStatus = "Unknown" ) // GatewayConditionType is a type of condition associated with a // Gateway. This type should be used with the GatewayStatus.Conditions // field. type GatewayConditionType string // GatewayConditionReason defines the set of reasons that explain why a // particular Gateway condition type has been raised. type GatewayConditionReason string // the following are directly from the k8s spec const ( // This condition is true when the controller managing the Gateway is // syntactically and semantically valid enough to produce some configuration // in the underlying data plane. This does not indicate whether or not the // configuration has been propagated to the data plane. // // Possible reasons for this condition to be True are: // // * "Accepted" // // Possible reasons for this condition to be False are: // // * InvalidCertificates // GatewayConditionAccepted GatewayConditionType = "Accepted" // This reason is used with the "Accepted" condition when the condition is // True. GatewayReasonAccepted GatewayConditionReason = "Accepted" // This reason is used with the "Accepted" condition when the gateway has multiple invalid // certificates and cannot bind to any routes GatewayReasonInvalidCertificates GatewayConditionReason = "InvalidCertificates" // This reason is used with the "Accepted" condition when the gateway has multiple invalid // JWT providers and cannot bind to any routes GatewayReasonInvalidJWTProviders GatewayConditionReason = "InvalidJWTProviders" // This condition indicates that the gateway was unable to resolve // conflicting specification requirements for this Listener. If a // Listener is conflicted, its network port should not be configured // on any network elements. // // Possible reasons for this condition to be true are: // // * "RouteConflict" // // Possible reasons for this condition to be False are: // // * "NoConflict" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. GatewayConditionConflicted GatewayConditionType = "Conflicted" // This reason is used with the "Conflicted" condition when the condition // is False. GatewayReasonNoConflict GatewayConditionReason = "NoConflict" // This reason is used with the "Conflicted" condition when the route is // in a conflicted state, such as when a TCPListener attempts to bind to two routes GatewayReasonRouteConflict GatewayConditionReason = "RouteConflict" // This condition indicates whether the controller was able to // resolve all the object references for the Gateway. When setting this // condition to False, a ResourceReference to the misconfigured Listener should // be provided. // // Possible reasons for this condition to be true are: // // * "ResolvedRefs" // // Possible reasons for this condition to be False are: // // * "InvalidCertificateRef" // * "InvalidRouteKinds" // * "RefNotPermitted" // GatewayConditionResolvedRefs GatewayConditionType = "ResolvedRefs" // This reason is used with the "ResolvedRefs" condition when the condition // is true. GatewayReasonResolvedRefs GatewayConditionReason = "ResolvedRefs" // This reason is used with the "ResolvedRefs" condition when a // Listener has a TLS configuration with at least one TLS CertificateRef // that is invalid or does not exist. // A CertificateRef is considered invalid when it refers to a nonexistent // or unsupported resource or kind, or when the data within that resource // is malformed. // This reason must be used only when the reference is allowed, either by // referencing an object in the same namespace as the Gateway, or when // a cross-namespace reference has been explicitly allowed by a ReferenceGrant. // If the reference is not allowed, the reason RefNotPermitted must be used // instead. GatewayListenerReasonInvalidCertificateRef GatewayConditionReason = "InvalidCertificateRef" // This reason is used with the "ResolvedRefs" condition when a // Listener has a JWT configuration with at least one JWTProvider // that is invalid or does not exist. // A JWTProvider is considered invalid when it refers to a nonexistent // or unsupported resource or kind, or when the data within that resource // is malformed. GatewayListenerReasonInvalidJWTProviderRef GatewayConditionReason = "InvalidJWTProviderRef" ) var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[ConditionStatus][]GatewayConditionReason{ GatewayConditionAccepted: { ConditionStatusTrue: { GatewayReasonAccepted, }, ConditionStatusFalse: { GatewayReasonInvalidCertificates, GatewayReasonInvalidJWTProviders, }, ConditionStatusUnknown: {}, }, GatewayConditionConflicted: { ConditionStatusTrue: { GatewayReasonRouteConflict, }, ConditionStatusFalse: { GatewayReasonNoConflict, }, ConditionStatusUnknown: {}, }, GatewayConditionResolvedRefs: { ConditionStatusTrue: { GatewayReasonResolvedRefs, }, ConditionStatusFalse: { GatewayListenerReasonInvalidCertificateRef, GatewayListenerReasonInvalidJWTProviderRef, }, ConditionStatusUnknown: {}, }, } func ValidateGatewayConditionReason(name GatewayConditionType, status ConditionStatus, reason GatewayConditionReason) error { if err := checkConditionStatus(status); err != nil { return err } reasons, ok := validGatewayConditionReasonsMapping[name] if !ok { return fmt.Errorf("unrecognized GatewayConditionType %q", name) } reasonsForStatus, ok := reasons[status] if !ok { return fmt.Errorf("unrecognized ConditionStatus %q", status) } if !slices.Contains(reasonsForStatus, reason) { return fmt.Errorf("gateway condition reason %q not allowed for gateway condition type %q with status %q", reason, name, status) } return nil } // RouteConditionType is a type of condition for a route. type RouteConditionType string // RouteConditionReason is a reason for a route condition. type RouteConditionReason string // The following statuses are taken from the K8's Spec // With the exception of: "RouteReasonInvalidDiscoveryChain" and "NoUpstreamServicesTargeted" const ( // This condition indicates whether the route has been accepted or rejected // by a Gateway, and why. // // Possible reasons for this condition to be true are: // // * "Accepted" // // Possible reasons for this condition to be False are: // // * "InvalidDiscoveryChain" // * "NoUpstreamServicesTargeted" // // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. RouteConditionAccepted RouteConditionType = "Accepted" // This reason is used with the "Accepted" condition when the Route has been // accepted by the Gateway. RouteReasonAccepted RouteConditionReason = "Accepted" // This reason is used with the "Accepted" condition when the route has an // invalid discovery chain, this includes conditions like the protocol being invalid // or the discovery chain failing to compile RouteReasonInvalidDiscoveryChain RouteConditionReason = "InvalidDiscoveryChain" // This reason is used with the "Accepted" condition when the route RouteReasonNoUpstreamServicesTargeted RouteConditionReason = "NoUpstreamServicesTargeted" ) // the following statuses are custom to Consul const ( // This condition indicates whether the route was able to successfully bind the // Listener on the gateway // Possible reasons for this condition to be true are: // // * "Bound" // // Possible reasons for this condition to be false are: // // * "FailedToBind" // * "GatewayNotFound" // RouteConditionBound RouteConditionType = "Bound" // This reason is used with the "Bound" condition when the condition // is true RouteReasonBound RouteConditionReason = "Bound" // This reason is used with the "Bound" condition when the route failed // to bind to the gateway RouteReasonFailedToBind RouteConditionReason = "FailedToBind" // This reason is used with the "Bound" condition when the route fails // to find the gateway RouteReasonGatewayNotFound RouteConditionReason = "GatewayNotFound" // This reason is used with the "Accepted" condition when the route references non-existent // JWTProviders RouteReasonJWTProvidersNotFound RouteConditionReason = "JWTProvidersNotFound" ) var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStatus][]RouteConditionReason{ RouteConditionAccepted: { ConditionStatusTrue: { RouteReasonAccepted, }, ConditionStatusFalse: { RouteReasonInvalidDiscoveryChain, RouteReasonNoUpstreamServicesTargeted, }, ConditionStatusUnknown: {}, }, RouteConditionBound: { ConditionStatusTrue: { RouteReasonBound, }, ConditionStatusFalse: { RouteReasonGatewayNotFound, RouteReasonFailedToBind, RouteReasonJWTProvidersNotFound, }, ConditionStatusUnknown: {}, }, } func ValidateRouteConditionReason(name RouteConditionType, status ConditionStatus, reason RouteConditionReason) error { if err := checkConditionStatus(status); err != nil { return err } reasons, ok := validRouteConditionReasonsMapping[name] if !ok { return fmt.Errorf("unrecognized RouteConditionType %s", name) } reasonsForStatus, ok := reasons[status] if !ok { return fmt.Errorf("unrecognized ConditionStatus %s", name) } if !slices.Contains(reasonsForStatus, reason) { return fmt.Errorf("route condition reason %s not allowed for route condition type %s with status %s", reason, name, status) } return nil } func checkConditionStatus(status ConditionStatus) error { switch status { case ConditionStatusTrue, ConditionStatusFalse, ConditionStatusUnknown: return nil default: return fmt.Errorf("unrecognized condition status: %q", status) } }