mirror of https://github.com/knative/pkg.git
451 lines
13 KiB
Go
451 lines
13 KiB
Go
/*
|
|
Copyright 2019 The Knative Authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package apis
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
// ConditionsAccessor is the interface for a Resource that implements the getter and
|
|
// setter for accessing a Condition collection.
|
|
// +k8s:deepcopy-gen=true
|
|
type ConditionsAccessor interface {
|
|
GetConditions() Conditions
|
|
SetConditions(Conditions)
|
|
}
|
|
|
|
// ConditionAccessor is used to access a condition through it's type
|
|
type ConditionAccessor interface {
|
|
// GetCondition finds and returns the Condition that matches the ConditionType
|
|
// It should return nil if the condition type is not present
|
|
GetCondition(t ConditionType) *Condition
|
|
}
|
|
|
|
// ConditionSet is an abstract collection of the possible ConditionType values
|
|
// that a particular resource might expose. It also holds the "happy condition"
|
|
// for that resource, which we define to be one of Ready or Succeeded depending
|
|
// on whether it is a Living or Batch process respectively.
|
|
// +k8s:deepcopy-gen=false
|
|
type ConditionSet struct {
|
|
happy ConditionType
|
|
dependents []ConditionType
|
|
}
|
|
|
|
// ConditionManager allows a resource to operate on its Conditions using higher
|
|
// order operations.
|
|
type ConditionManager interface {
|
|
ConditionAccessor
|
|
|
|
// IsHappy looks at the happy condition and returns true if that condition is
|
|
// set to true.
|
|
IsHappy() bool
|
|
|
|
// GetTopLevelCondition finds and returns the top level Condition (happy Condition).
|
|
GetTopLevelCondition() *Condition
|
|
|
|
// SetCondition sets or updates the Condition on Conditions for Condition.Type.
|
|
// If there is an update, Conditions are stored back sorted.
|
|
SetCondition(new Condition)
|
|
|
|
// ClearCondition removes the non terminal condition that matches the ConditionType
|
|
ClearCondition(t ConditionType) error
|
|
|
|
// MarkTrue sets the status of t to true, and then marks the happy condition to
|
|
// true if all dependents are true.
|
|
MarkTrue(t ConditionType)
|
|
|
|
// MarkTrueWithReason sets the status of t to true with the reason, and then marks the happy
|
|
// condition to true if all dependents are true.
|
|
MarkTrueWithReason(t ConditionType, reason, messageFormat string, messageA ...interface{})
|
|
|
|
// MarkUnknown sets the status of t to Unknown and also sets the happy condition
|
|
// to Unknown if no other dependent condition is in an error state.
|
|
MarkUnknown(t ConditionType, reason, messageFormat string, messageA ...interface{})
|
|
|
|
// MarkFalse sets the status of t and the happy condition to False.
|
|
MarkFalse(t ConditionType, reason, messageFormat string, messageA ...interface{})
|
|
|
|
// InitializeConditions updates all Conditions in the ConditionSet to Unknown
|
|
// if not set.
|
|
InitializeConditions()
|
|
}
|
|
|
|
// NewLivingConditionSet returns a ConditionSet to hold the conditions for the
|
|
// living resource. ConditionReady is used as the happy condition.
|
|
// The set of condition types provided are those of the terminal subconditions.
|
|
func NewLivingConditionSet(d ...ConditionType) ConditionSet {
|
|
return newConditionSet(ConditionReady, d...)
|
|
}
|
|
|
|
// NewBatchConditionSet returns a ConditionSet to hold the conditions for the
|
|
// batch resource. ConditionSucceeded is used as the happy condition.
|
|
// The set of condition types provided are those of the terminal subconditions.
|
|
func NewBatchConditionSet(d ...ConditionType) ConditionSet {
|
|
return newConditionSet(ConditionSucceeded, d...)
|
|
}
|
|
|
|
// newConditionSet returns a ConditionSet to hold the conditions that are
|
|
// important for the caller. The first ConditionType is the overarching status
|
|
// for that will be used to signal the resources' status is Ready or Succeeded.
|
|
func newConditionSet(happy ConditionType, dependents ...ConditionType) ConditionSet {
|
|
deps := make([]ConditionType, 0, len(dependents))
|
|
for _, d := range dependents {
|
|
// Skip duplicates
|
|
if d == happy || contains(deps, d) {
|
|
continue
|
|
}
|
|
deps = append(deps, d)
|
|
}
|
|
return ConditionSet{
|
|
happy: happy,
|
|
dependents: deps,
|
|
}
|
|
}
|
|
|
|
func contains(ct []ConditionType, t ConditionType) bool {
|
|
for _, c := range ct {
|
|
if c == t {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check that conditionsImpl implements ConditionManager.
|
|
var _ ConditionManager = (*conditionsImpl)(nil)
|
|
|
|
// conditionsImpl implements the helper methods for evaluating Conditions.
|
|
// +k8s:deepcopy-gen=false
|
|
type conditionsImpl struct {
|
|
ConditionSet
|
|
accessor ConditionsAccessor
|
|
}
|
|
|
|
// GetTopLevelConditionType is an accessor for the top-level happy condition.
|
|
func (r ConditionSet) GetTopLevelConditionType() ConditionType {
|
|
return r.happy
|
|
}
|
|
|
|
// Manage creates a ConditionManager from an accessor object using the original
|
|
// ConditionSet as a reference. Status must be a pointer to a struct.
|
|
func (r ConditionSet) Manage(status ConditionsAccessor) ConditionManager {
|
|
return conditionsImpl{
|
|
accessor: status,
|
|
ConditionSet: r,
|
|
}
|
|
}
|
|
|
|
// IsHappy looks at the top level Condition (happy Condition) and returns true if that condition is
|
|
// set to true.
|
|
func (r conditionsImpl) IsHappy() bool {
|
|
return r.GetTopLevelCondition().IsTrue()
|
|
}
|
|
|
|
// GetTopLevelCondition finds and returns the top level Condition (happy Condition).
|
|
func (r conditionsImpl) GetTopLevelCondition() *Condition {
|
|
return r.GetCondition(r.happy)
|
|
}
|
|
|
|
// GetCondition finds and returns the Condition that matches the ConditionType
|
|
// previously set on Conditions.
|
|
func (r conditionsImpl) GetCondition(t ConditionType) *Condition {
|
|
if r.accessor == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, c := range r.accessor.GetConditions() {
|
|
if c.Type == t {
|
|
return &c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetCondition sets or updates the Condition on Conditions for Condition.Type.
|
|
// If there is an update, Conditions are stored back sorted.
|
|
func (r conditionsImpl) SetCondition(cond Condition) {
|
|
if r.accessor == nil {
|
|
return
|
|
}
|
|
t := cond.Type
|
|
var conditions Conditions
|
|
for _, c := range r.accessor.GetConditions() {
|
|
if c.Type != t {
|
|
conditions = append(conditions, c)
|
|
} else {
|
|
// If we'd only update the LastTransitionTime, then return.
|
|
cond.LastTransitionTime = c.LastTransitionTime
|
|
if reflect.DeepEqual(cond, c) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
cond.LastTransitionTime = VolatileTime{Inner: metav1.NewTime(time.Now())}
|
|
conditions = append(conditions, cond)
|
|
// Sorted for convenience of the consumer, i.e. kubectl.
|
|
sort.Slice(conditions, func(i, j int) bool { return conditions[i].Type < conditions[j].Type })
|
|
r.accessor.SetConditions(conditions)
|
|
}
|
|
|
|
func (r conditionsImpl) isTerminal(t ConditionType) bool {
|
|
for _, cond := range r.dependents {
|
|
if cond == t {
|
|
return true
|
|
}
|
|
}
|
|
return t == r.happy
|
|
}
|
|
|
|
func (r conditionsImpl) severity(t ConditionType) ConditionSeverity {
|
|
if r.isTerminal(t) {
|
|
return ConditionSeverityError
|
|
}
|
|
return ConditionSeverityInfo
|
|
}
|
|
|
|
// RemoveCondition removes the non terminal condition that matches the ConditionType
|
|
// Not implemented for terminal conditions
|
|
func (r conditionsImpl) ClearCondition(t ConditionType) error {
|
|
var conditions Conditions
|
|
|
|
if r.accessor == nil {
|
|
return nil
|
|
}
|
|
// Terminal conditions are not handled as they can't be nil
|
|
if r.isTerminal(t) {
|
|
return errors.New("clearing terminal conditions not implemented")
|
|
}
|
|
cond := r.GetCondition(t)
|
|
if cond == nil {
|
|
return nil
|
|
}
|
|
for _, c := range r.accessor.GetConditions() {
|
|
if c.Type != t {
|
|
conditions = append(conditions, c)
|
|
}
|
|
}
|
|
|
|
// Sorted for convenience of the consumer, i.e. kubectl.
|
|
sort.Slice(conditions, func(i, j int) bool { return conditions[i].Type < conditions[j].Type })
|
|
r.accessor.SetConditions(conditions)
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarkTrue sets the status of t to true, and then marks the happy condition to
|
|
// true if all other dependents are also true.
|
|
func (r conditionsImpl) MarkTrue(t ConditionType) {
|
|
// Set the specified condition.
|
|
r.SetCondition(Condition{
|
|
Type: t,
|
|
Status: corev1.ConditionTrue,
|
|
Severity: r.severity(t),
|
|
})
|
|
r.recomputeHappiness(t)
|
|
}
|
|
|
|
// MarkTrueWithReason sets the status of t to true with the reason, and then marks the happy condition to
|
|
// true if all other dependents are also true.
|
|
func (r conditionsImpl) MarkTrueWithReason(t ConditionType, reason, messageFormat string, messageA ...interface{}) {
|
|
// set the specified condition
|
|
r.SetCondition(Condition{
|
|
Type: t,
|
|
Status: corev1.ConditionTrue,
|
|
Reason: reason,
|
|
Message: fmt.Sprintf(messageFormat, messageA...),
|
|
Severity: r.severity(t),
|
|
})
|
|
r.recomputeHappiness(t)
|
|
}
|
|
|
|
// recomputeHappiness marks the happy condition to true if all other dependents are also true.
|
|
func (r conditionsImpl) recomputeHappiness(t ConditionType) {
|
|
if c := r.findUnhappyDependent(); c != nil {
|
|
// Propagate unhappy dependent to happy condition.
|
|
r.SetCondition(Condition{
|
|
Type: r.happy,
|
|
Status: c.Status,
|
|
Reason: c.Reason,
|
|
Message: c.Message,
|
|
Severity: r.severity(r.happy),
|
|
})
|
|
} else if t != r.happy {
|
|
// Set the happy condition to true.
|
|
r.SetCondition(Condition{
|
|
Type: r.happy,
|
|
Status: corev1.ConditionTrue,
|
|
Severity: r.severity(r.happy),
|
|
})
|
|
}
|
|
}
|
|
|
|
func (r conditionsImpl) findUnhappyDependent() *Condition {
|
|
// This only works if there are dependents.
|
|
if len(r.dependents) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Do not modify the accessors condition order.
|
|
conditions := r.accessor.GetConditions().DeepCopy()
|
|
|
|
// Filter based on terminal status.
|
|
n := 0
|
|
for _, c := range conditions {
|
|
if c.Severity == ConditionSeverityError && c.Type != r.happy {
|
|
conditions[n] = c
|
|
n++
|
|
}
|
|
}
|
|
conditions = conditions[:n]
|
|
|
|
// Sort set conditions by time.
|
|
sort.Slice(conditions, func(i, j int) bool {
|
|
return conditions[i].LastTransitionTime.Inner.Time.After(conditions[j].LastTransitionTime.Inner.Time)
|
|
})
|
|
|
|
// First check the conditions with Status == False.
|
|
for _, c := range conditions {
|
|
// False conditions trump Unknown.
|
|
if c.IsFalse() {
|
|
return &c
|
|
}
|
|
}
|
|
// Second check for conditions with Status == Unknown.
|
|
for _, c := range conditions {
|
|
if c.IsUnknown() {
|
|
return &c
|
|
}
|
|
}
|
|
|
|
// If something was not initialized.
|
|
if len(r.dependents) > len(conditions) {
|
|
return &Condition{
|
|
Status: corev1.ConditionUnknown,
|
|
}
|
|
}
|
|
|
|
// All dependents are fine.
|
|
return nil
|
|
}
|
|
|
|
// MarkUnknown sets the status of t to Unknown and also sets the happy condition
|
|
// to Unknown if no other dependent condition is in an error state.
|
|
func (r conditionsImpl) MarkUnknown(t ConditionType, reason, messageFormat string, messageA ...interface{}) {
|
|
// set the specified condition
|
|
r.SetCondition(Condition{
|
|
Type: t,
|
|
Status: corev1.ConditionUnknown,
|
|
Reason: reason,
|
|
Message: fmt.Sprintf(messageFormat, messageA...),
|
|
Severity: r.severity(t),
|
|
})
|
|
|
|
// check the dependents.
|
|
isDependent := false
|
|
for _, cond := range r.dependents {
|
|
c := r.GetCondition(cond)
|
|
// Failed conditions trump Unknown conditions
|
|
if c.IsFalse() {
|
|
// Double check that the happy condition is also false.
|
|
happy := r.GetCondition(r.happy)
|
|
if !happy.IsFalse() {
|
|
r.MarkFalse(r.happy, reason, messageFormat, messageA...)
|
|
}
|
|
return
|
|
}
|
|
if cond == t {
|
|
isDependent = true
|
|
}
|
|
}
|
|
|
|
if isDependent {
|
|
// set the happy condition, if it is one of our dependent subconditions.
|
|
r.SetCondition(Condition{
|
|
Type: r.happy,
|
|
Status: corev1.ConditionUnknown,
|
|
Reason: reason,
|
|
Message: fmt.Sprintf(messageFormat, messageA...),
|
|
Severity: r.severity(r.happy),
|
|
})
|
|
}
|
|
}
|
|
|
|
// MarkFalse sets the status of t and the happy condition to False.
|
|
func (r conditionsImpl) MarkFalse(t ConditionType, reason, messageFormat string, messageA ...interface{}) {
|
|
types := []ConditionType{t}
|
|
for _, cond := range r.dependents {
|
|
if cond == t {
|
|
types = append(types, r.happy)
|
|
}
|
|
}
|
|
|
|
for _, t := range types {
|
|
r.SetCondition(Condition{
|
|
Type: t,
|
|
Status: corev1.ConditionFalse,
|
|
Reason: reason,
|
|
Message: fmt.Sprintf(messageFormat, messageA...),
|
|
Severity: r.severity(t),
|
|
})
|
|
}
|
|
}
|
|
|
|
// InitializeConditions updates all Conditions in the ConditionSet to Unknown
|
|
// if not set.
|
|
func (r conditionsImpl) InitializeConditions() {
|
|
happy := r.GetCondition(r.happy)
|
|
if happy == nil {
|
|
happy = &Condition{
|
|
Type: r.happy,
|
|
Status: corev1.ConditionUnknown,
|
|
Severity: ConditionSeverityError,
|
|
}
|
|
r.SetCondition(*happy)
|
|
}
|
|
// If the happy state is true, it implies that all of the terminal
|
|
// subconditions must be true, so initialize any unset conditions to
|
|
// true if our happy condition is true, otherwise unknown.
|
|
status := corev1.ConditionUnknown
|
|
if happy.Status == corev1.ConditionTrue {
|
|
status = corev1.ConditionTrue
|
|
}
|
|
for _, t := range r.dependents {
|
|
r.initializeTerminalCondition(t, status)
|
|
}
|
|
}
|
|
|
|
// initializeTerminalCondition initializes a Condition to the given status if unset.
|
|
func (r conditionsImpl) initializeTerminalCondition(t ConditionType, status corev1.ConditionStatus) *Condition {
|
|
if c := r.GetCondition(t); c != nil {
|
|
return c
|
|
}
|
|
c := Condition{
|
|
Type: t,
|
|
Status: status,
|
|
Severity: ConditionSeverityError,
|
|
}
|
|
r.SetCondition(c)
|
|
return &c
|
|
}
|