v2/pkg/genruntime/conditions/conditions.go (147 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package conditions
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Conditions []Condition
// FindIndexByType returns the index of the condition with the given ConditionType if it exists.
// If the Condition with the specified ConditionType is doesn't exist, the second boolean parameter is false.
func (c Conditions) FindIndexByType(conditionType ConditionType) (int, bool) {
for i := range c {
condition := c[i]
if condition.Type == conditionType {
return i, true
}
}
return -1, false
}
// TODO: Hah, name...
type Conditioner interface {
GetConditions() Conditions
SetConditions(conditions Conditions)
}
// ConditionSeverity expresses the severity of a Condition.
type ConditionSeverity string
const (
// ConditionSeverityError specifies that a failure of a condition type
// should be viewed as an error. Errors are fatal to reconciliation and
// mean that the user must take some action to resolve
// the problem before reconciliation will be attempted again.
ConditionSeverityError ConditionSeverity = "Error"
// ConditionSeverityWarning specifies that a failure of a condition type
// should be viewed as a warning. Warnings are informational. The operator
// may be able to retry and resolve the warning without any action from the user, but
// in some cases user action to resolve the warning will be required.
ConditionSeverityWarning ConditionSeverity = "Warning"
// ConditionSeverityInfo specifies that a failure of a condition type
// should be viewed as purely informational. Things are working.
// This is the happy path.
ConditionSeverityInfo ConditionSeverity = "Info"
// ConditionSeverityNone specifies that there is no condition severity.
// For conditions which have positive polarity (Status == True is their normal/healthy state), this will set when Status == True
// For conditions which have negative polarity (Status == False is their normal/healthy state), this will be set when Status == False.
// Conditions in Status == Unknown always have a severity of None as well.
// This is the default state for conditions.
ConditionSeverityNone ConditionSeverity = ""
)
type ConditionType string
const (
// ConditionTypeReady is a condition indicating if the resource is ready or not.
// A ready resource is one that has been successfully provisioned to Azure according to the
// resource spec. It has reached the goal state. This usually means that the resource is ready
// to use, but the exact meaning of Ready may vary slightly from resource to resource. Resources with
// caveats to Ready's meaning will call that out in the resource specific documentation.
ConditionTypeReady = "Ready"
)
var _ fmt.Stringer = Condition{}
// Condition defines an extension to status (an observation) of a resource
// +kubebuilder:object:generate=true
//
//nolint:recvcheck
type Condition struct {
// Type of condition.
// +kubebuilder:validation:Required
Type ConditionType `json:"type"`
// Status of the condition, one of True, False, or Unknown.
// +kubebuilder:validation:Required
Status metav1.ConditionStatus `json:"status"`
// Severity with which to treat failures of this type of condition.
// For conditions which have positive polarity (Status == True is their normal/healthy state), this will be omitted when Status == True
// For conditions which have negative polarity (Status == False is their normal/healthy state), this will be omitted when Status == False.
// This is omitted in all cases when Status == Unknown
// +kubebuilder:validation:Optional
Severity ConditionSeverity `json:"severity,omitempty"`
// LastTransitionTime is the last time the condition transitioned from one status to another.
// +kubebuilder:validation:Required
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
// Note: see the https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1623-standardize-conditions
// KEP for details about ObservedGeneration
// ObservedGeneration is the .metadata.generation that the condition was set based upon. For instance, if
// .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date
// with respect to the current state of the instance.
// +kubebuilder:validation:Optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Reason for the condition's last transition.
// Reasons are upper CamelCase (PascalCase) with no spaces. A reason is always provided, this field will not be empty.
// +kubebuilder:validation:Required
Reason string `json:"reason"`
// Message is a human readable message indicating details about the transition. This field may be empty.
// +kubebuilder:validation:Optional
Message string `json:"message,omitempty"`
}
// IsEquivalent returns true if this condition is equivalent to the passed in condition.
// Two conditions are equivalent if all of their fields EXCEPT LastTransitionTime are the same.
func (c *Condition) IsEquivalent(other Condition) bool {
return c.Type == other.Type &&
c.Status == other.Status &&
c.Severity == other.Severity &&
c.Reason == other.Reason &&
c.ObservedGeneration == other.ObservedGeneration &&
c.Message == other.Message
}
// ShouldOverwrite determines if this condition should overwrite the other condition.
func (c *Condition) ShouldOverwrite(other Condition) bool {
// Safety check that the two conditions are of the same type. If not they certainly shouldn't overwrite
if c.Type != other.Type {
return false
}
// If this condition corresponds to a newer generation than the previous condition always overwrite
// as other is out of date
if c.ObservedGeneration > other.ObservedGeneration {
return true
}
// If the Conditions are equivalent, don't overwrite. We want to keep the first occurrence of the condition
// so that the LastTransitionTime is correct
if c.IsEquivalent(other) {
return false
}
// At this point the Conditions are the same type, same generation, so the winning Condition must be chosen
// based on priority
if c.priority() >= other.priority() {
return true
} else {
return false
}
}
// priority of the condition for overwrite purposes if Condition ObservedGeneration's are the same. Higher is more important.
// The result of this is the following:
// 1. Status == True conditions, and Status == False conditions with Severity == Warning or Error are all the highest priority.
// This means that the most recent update with any of those states will overwrite anything.
// 2. Status == False conditions with Severity == Info will only overwrite other Status == False conditions with Severity == Info.
// 3. Status == Unknown conditions will not overwrite anything.
//
// Keep in mind that this priority is specifically for comparing Conditions with the same ObservedGeneration. If the ObservedGeneration
// is different, the newer one always wins.
func (c *Condition) priority() int {
switch c.Status {
case metav1.ConditionTrue:
return 5
case metav1.ConditionFalse:
switch c.Severity {
case ConditionSeverityError:
return 5
case ConditionSeverityWarning:
return 5
case ConditionSeverityInfo:
return 4
case ConditionSeverityNone:
// This shouldn't happen as a Condition with Status False should always specify a severity.
// In the interest of safety though, we set this to 5 so if this DOES somehow happen it ties
// or wins against most other things and users will see it
return 5
}
case metav1.ConditionUnknown:
return 3
}
// This shouldn't happen
return 0
}
// Copy returns an independent copy of the Condition
func (c *Condition) Copy() Condition {
// NB: If you change this to a non-simple copy
// you will need to update genruntime.CloneSliceOfCondition
return *c
}
// String returns a string representation of this condition
func (c Condition) String() string {
return fmt.Sprintf(
"Condition [%s], Status = %q, ObservedGeneration = %d, Severity = %q, Reason = %q, Message = %q, LastTransitionTime = %q",
c.Type,
c.Status,
c.ObservedGeneration,
c.Severity,
c.Reason,
c.Message,
c.LastTransitionTime)
}
// SetCondition sets the provided Condition on the Conditioner. The condition is only
// set if the new condition is different from the existing condition of the same type.
// See Condition.IsEquivalent and Condition.ShouldOverwrite for more details.
func SetCondition(o Conditioner, new Condition) {
setCondition(o, new, func(new Condition, old Condition) bool { return new.ShouldOverwrite(old) })
}
// Reasons other than those explicitly called out here have the default priority of 0.
// These are given a negative priority so that they "lose" to the default of 0 and are overwritten.
// Take care to not modify this structure (Golang doesn't support a readonly map). We could use
// v2/tools/generator/internal/readonly/readonly_map.go but given
// Golang generics bugs for now we go with the simpler approach
var reasonPriority = map[string]int{
ReasonReferenceNotFound.Name: -2,
ReasonSecretNotFound.Name: -2,
ReasonConfigMapNotFound.Name: -2,
// AzureResourceNotFound only comes up when ReconcilePolicy is skip. This conditions priority being less than
// Reconciling allows skip -> reconcile to immediately update the condition to Reconciling rather than continuing to
// report AzureResourceNotFound until the resource is created.
ReasonAzureResourceNotFound.Name: -2,
ReasonWaitingForOwner.Name: -2,
ReasonReconciling.Name: -1,
}
// SetConditionReasonAware sets the provided Condition on the Conditioner. This is similar to SetCondition
// with one difference: SetConditionReasonAware understands common Reasons used by ASO and allows some of them to
// modify the standard Condition priority rules. This is primarily used to allow the Reconciling condition to overwrite
// Warning conditions raised by the operator that have been fixed. This is useful because sometimes getting a success or
// error from Azure can take a long time, and workflows like: submit -> warning -> fix warning -> call Azure -> wait -> success
// otherwise would continue reporting the Warning Condition until the final success step (possibly many minutes after the
// warning was resolved).
func SetConditionReasonAware(o Conditioner, new Condition) {
shouldOverwrite := func(new Condition, old Condition) bool {
if new.ShouldOverwrite(old) {
return true
}
// If we normally wouldn't overwrite, check the reason of the old and new condition and compare their priorities
oldPriority := reasonPriority[old.Reason] // Default is 0 if not mapped
newPriority := reasonPriority[new.Reason] // Default is 0 if not mapped
return newPriority > oldPriority // Just > rather than >= here to prevent things overwriting themselves
}
setCondition(o, new, shouldOverwrite)
}
func setCondition(o Conditioner, new Condition, shouldOverwrite func(new Condition, old Condition) bool) {
if o == nil {
return
}
conditions := o.GetConditions()
i, exists := conditions.FindIndexByType(new.Type)
if exists {
if !shouldOverwrite(new, conditions[i]) {
// Nothing to do, the new condition is not supposed to overwrite
return
}
conditions[i] = new
} else {
conditions = append(conditions, new)
}
// TODO: do we sort conditions here? CAPI does.
o.SetConditions(conditions)
}
// GetCondition gets the Condition with the specified type from the provided Conditioner.
// Returns the Condition and true if a Condition with the specified type is found, or an empty Condition
// and false if not.
func GetCondition(o Conditioner, conditionType ConditionType) (Condition, bool) {
if o == nil {
return Condition{}, false
}
conditions := o.GetConditions()
i, exists := conditions.FindIndexByType(conditionType)
if exists {
return conditions[i], true
}
return Condition{}, false
}