open-consul/api/config_entry_status.go
John Maguire d19a7dad68
APIGW: Update how status conditions for certificates are handled (#17115)
* Move status condition for invalid certifcate to reference the listener
that is using the certificate

* Fix where we set the condition status for listeners and certificate
refs, added tests

* Add changelog
2023-04-27 15:54:44 +00:00

340 lines
11 KiB
Go

// 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 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"
)
var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[ConditionStatus][]GatewayConditionReason{
GatewayConditionAccepted: {
ConditionStatusTrue: {
GatewayReasonAccepted,
},
ConditionStatusFalse: {
GatewayReasonInvalidCertificates,
},
ConditionStatusUnknown: {},
},
GatewayConditionConflicted: {
ConditionStatusTrue: {
GatewayReasonRouteConflict,
},
ConditionStatusFalse: {
GatewayReasonNoConflict,
},
ConditionStatusUnknown: {},
},
GatewayConditionResolvedRefs: {
ConditionStatusTrue: {
GatewayReasonResolvedRefs,
},
ConditionStatusFalse: {
GatewayListenerReasonInvalidCertificateRef,
},
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"
)
var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStatus][]RouteConditionReason{
RouteConditionAccepted: {
ConditionStatusTrue: {
RouteReasonAccepted,
},
ConditionStatusFalse: {
RouteReasonInvalidDiscoveryChain,
RouteReasonNoUpstreamServicesTargeted,
},
ConditionStatusUnknown: {},
},
RouteConditionBound: {
ConditionStatusTrue: {
RouteReasonBound,
},
ConditionStatusFalse: {
RouteReasonGatewayNotFound,
RouteReasonFailedToBind,
},
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)
}
}