open-consul/agent/structs/config_entry_status.go

239 lines
6.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package structs
import (
"fmt"
"sort"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/api"
)
// 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
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
}
func (r *ResourceReference) String() string {
return fmt.Sprintf("%s:%s/%s/%s/%s", r.Kind, r.PartitionOrDefault(), r.NamespaceOrDefault(), r.Name, r.SectionName)
}
func (r *ResourceReference) IsSame(other *ResourceReference) bool {
if r == nil && other == nil {
return true
}
if r == nil || other == nil {
return false
}
return r.Kind == other.Kind &&
r.Name == other.Name &&
r.SectionName == other.SectionName &&
r.EnterpriseMeta.IsSame(&other.EnterpriseMeta)
}
// Status is used for propagating back asynchronously calculated
// messages from control loops to a user
type Status struct {
// Conditions is the set of condition objects associated with
// a ConfigEntry status.
Conditions []Condition
}
func (s *Status) MatchesConditionStatus(condition Condition) bool {
for _, c := range s.Conditions {
if c.IsCondition(&condition) &&
c.Status == condition.Status {
return true
}
}
return false
}
func (s Status) SameConditions(other Status) bool {
if len(s.Conditions) != len(other.Conditions) {
return false
}
sortConditions := func(conditions []Condition) []Condition {
sort.SliceStable(conditions, func(i, j int) bool {
if conditions[i].Type < conditions[j].Type {
return true
}
if conditions[i].Type > conditions[j].Type {
return false
}
return lessResource(conditions[i].Resource, conditions[j].Resource)
})
return conditions
}
oneConditions := sortConditions(s.Conditions)
twoConditions := sortConditions(other.Conditions)
for i, condition := range oneConditions {
other := twoConditions[i]
if !condition.IsSame(&other) {
return false
}
}
return true
}
func lessResource(one, two *ResourceReference) bool {
if one == nil && two == nil {
return false
}
if one == nil {
return true
}
if two == nil {
return false
}
if one.Kind < two.Kind {
return true
}
if one.Kind > two.Kind {
return false
}
if one.Name < two.Name {
return true
}
if one.Name > two.Name {
return false
}
return one.SectionName < two.SectionName
}
// 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 string
// 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
}
func (c *Condition) IsCondition(other *Condition) bool {
return c.Type == other.Type && c.Resource.IsSame(other.Resource)
}
func (c *Condition) IsSame(other *Condition) bool {
return c.IsCondition(other) &&
c.Status == other.Status &&
c.Reason == other.Reason &&
c.Message == other.Message
}
type StatusUpdater struct {
entry ControlledConfigEntry
status Status
}
func NewStatusUpdater(entry ControlledConfigEntry) *StatusUpdater {
status := entry.GetStatus()
return &StatusUpdater{
entry: entry,
status: *status.DeepCopy(),
}
}
func (u *StatusUpdater) SetCondition(condition Condition) {
for i, c := range u.status.Conditions {
if c.IsCondition(&condition) {
if !c.IsSame(&condition) {
// the conditions aren't identical, merge this one in
u.status.Conditions[i] = condition
}
// we either set the condition or it was already set, so
// just return
return
}
}
u.status.Conditions = append(u.status.Conditions, condition)
}
func (u *StatusUpdater) ClearConditions() {
u.status.Conditions = []Condition{}
}
func (u *StatusUpdater) RemoveCondition(condition Condition) {
filtered := []Condition{}
for _, c := range u.status.Conditions {
if !c.IsCondition(&condition) {
filtered = append(filtered, c)
}
}
u.status.Conditions = filtered
}
func (u *StatusUpdater) UpdateEntry() (ControlledConfigEntry, bool) {
if u.status.SameConditions(u.entry.GetStatus()) {
return nil, false
}
u.entry.SetStatus(u.status)
return u.entry, true
}
func NewGatewayCondition(name api.GatewayConditionType, status api.ConditionStatus, reason api.GatewayConditionReason, message string, resource ResourceReference) Condition {
if err := api.ValidateGatewayConditionReason(name, status, reason); err != nil {
// note we panic here because an invalid combination is a programmer error
// this should never actually be hit
panic(err)
}
return Condition{
Type: string(name),
Status: string(status),
Reason: string(reason),
Message: message,
Resource: ptrTo(resource),
LastTransitionTime: ptrTo(time.Now().UTC()),
}
}
// NewRouteCondition is a helper to build allowable Conditions for a Route config entry
func NewRouteCondition(name api.RouteConditionType, status api.ConditionStatus, reason api.RouteConditionReason, message string, ref ResourceReference) Condition {
if err := api.ValidateRouteConditionReason(name, status, reason); err != nil {
// note we panic here because an invalid combination is a programmer error
// this should never actually be hit
panic(err)
}
return Condition{
Type: string(name),
Status: string(status),
Reason: string(reason),
Message: message,
Resource: ptrTo(ref),
LastTransitionTime: ptrTo(time.Now().UTC()),
}
}
func ptrTo[T any](val T) *T {
return &val
}