...

Source file src/sigs.k8s.io/cli-utils/pkg/kstatus/status/status.go

Documentation: sigs.k8s.io/cli-utils/pkg/kstatus/status

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package status
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"time"
    10  
    11  	corev1 "k8s.io/api/core/v1"
    12  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    13  )
    14  
    15  const (
    16  	// The set of standard conditions defined in this package. These follow the "abnormality-true"
    17  	// convention where conditions should have a true value for abnormal/error situations and the absence
    18  	// of a condition should be interpreted as a false value, i.e. everything is normal.
    19  	ConditionStalled     ConditionType = "Stalled"
    20  	ConditionReconciling ConditionType = "Reconciling"
    21  
    22  	// The set of status conditions which can be assigned to resources.
    23  	InProgressStatus  Status = "InProgress"
    24  	FailedStatus      Status = "Failed"
    25  	CurrentStatus     Status = "Current"
    26  	TerminatingStatus Status = "Terminating"
    27  	NotFoundStatus    Status = "NotFound"
    28  	UnknownStatus     Status = "Unknown"
    29  )
    30  
    31  var (
    32  	Statuses = []Status{InProgressStatus, FailedStatus, CurrentStatus, TerminatingStatus, UnknownStatus}
    33  )
    34  
    35  // ConditionType defines the set of condition types allowed inside a Condition struct.
    36  type ConditionType string
    37  
    38  // String returns the ConditionType as a string.
    39  func (c ConditionType) String() string {
    40  	return string(c)
    41  }
    42  
    43  // Status defines the set of statuses a resource can have.
    44  type Status string
    45  
    46  // String returns the status as a string.
    47  func (s Status) String() string {
    48  	return string(s)
    49  }
    50  
    51  // StatusFromString turns a string into a Status. Will panic if the provided string is
    52  // not a valid status.
    53  func FromStringOrDie(text string) Status {
    54  	s := Status(text)
    55  	for _, r := range Statuses {
    56  		if s == r {
    57  			return s
    58  		}
    59  	}
    60  	panic(fmt.Errorf("string has invalid status: %s", s))
    61  }
    62  
    63  // Result contains the results of a call to compute the status of
    64  // a resource.
    65  type Result struct {
    66  	// Status
    67  	Status Status
    68  	// Message
    69  	Message string
    70  	// Conditions list of extracted conditions from Resource
    71  	Conditions []Condition
    72  }
    73  
    74  // Condition defines the general format for conditions on Kubernetes resources.
    75  // In practice, each kubernetes resource defines their own format for conditions, but
    76  // most (maybe all) follows this structure.
    77  type Condition struct {
    78  	// Type condition type
    79  	Type ConditionType `json:"type,omitempty"`
    80  	// Status String that describes the condition status
    81  	Status corev1.ConditionStatus `json:"status,omitempty"`
    82  	// Reason one work CamelCase reason
    83  	Reason string `json:"reason,omitempty"`
    84  	// Message Human readable reason string
    85  	Message string `json:"message,omitempty"`
    86  }
    87  
    88  // Compute finds the status of a given unstructured resource. It does not
    89  // fetch the state of the resource from a cluster, so the provided unstructured
    90  // must have the complete state, including status.
    91  //
    92  // The returned result contains the status of the resource, which will be
    93  // one of
    94  //   - InProgress
    95  //   - Current
    96  //   - Failed
    97  //   - Terminating
    98  //
    99  // It also contains a message that provides more information on why
   100  // the resource has the given status. Finally, the result also contains
   101  // a list of standard resources that would belong on the given resource.
   102  func Compute(u *unstructured.Unstructured) (*Result, error) {
   103  	res, err := checkGenericProperties(u)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	// If res is not nil, it means the generic checks was able to determine
   109  	// the status of the resource. We don't need to check the type-specific
   110  	// rules.
   111  	if res != nil {
   112  		return res, nil
   113  	}
   114  
   115  	fn := GetLegacyConditionsFn(u)
   116  	if fn != nil {
   117  		return fn(u)
   118  	}
   119  
   120  	// If neither the generic properties of the resource-specific rules
   121  	// can determine status, we do one last check to see if the resource
   122  	// does expose a Ready condition. Ready conditions do not adhere
   123  	// to the Kubernetes design recommendations, but they are pretty widely
   124  	// used.
   125  	res, err = checkReadyCondition(u)
   126  	if res != nil || err != nil {
   127  		return res, err
   128  	}
   129  
   130  	// The resource is not one of the built-in types with specific
   131  	// rules and we were unable to make a decision based on the
   132  	// generic rules. In this case we assume that the absence of any known
   133  	// conditions means the resource is current.
   134  	return &Result{
   135  		Status:     CurrentStatus,
   136  		Message:    "Resource is current",
   137  		Conditions: []Condition{},
   138  	}, err
   139  }
   140  
   141  // checkReadyCondition checks if a resource has a Ready condition, and
   142  // if so, it will use the value of this condition to determine the
   143  // status.
   144  // There are a few challenges with this:
   145  // - If a resource doesn't set the Ready condition until it is True,
   146  // the library have no way of telling whether the resource is using the
   147  // Ready condition, so it will fall back to the strategy for unknown
   148  // resources, which is to assume they are always reconciled.
   149  // - If the library sees the resource before the controller has had
   150  // a chance to update the conditions, it also will not realize the
   151  // resource use the Ready condition.
   152  // - There is no way to determine if a resource with the Ready condition
   153  // set to False is making progress or is doomed.
   154  func checkReadyCondition(u *unstructured.Unstructured) (*Result, error) {
   155  	objWithConditions, err := GetObjectWithConditions(u.Object)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	for _, cond := range objWithConditions.Status.Conditions {
   161  		if cond.Type != "Ready" {
   162  			continue
   163  		}
   164  		switch cond.Status {
   165  		case corev1.ConditionTrue:
   166  			return &Result{
   167  				Status:     CurrentStatus,
   168  				Message:    "Resource is Ready",
   169  				Conditions: []Condition{},
   170  			}, nil
   171  		case corev1.ConditionFalse:
   172  			return newInProgressStatus(cond.Reason, cond.Message), nil
   173  		case corev1.ConditionUnknown:
   174  			// For now we just treat an unknown condition value as
   175  			// InProgress. We should consider if there are better ways
   176  			// to handle it.
   177  			return newInProgressStatus(cond.Reason, cond.Message), nil
   178  		default:
   179  			// Do nothing in this case.
   180  		}
   181  	}
   182  	return nil, nil
   183  }
   184  
   185  // Augment takes a resource and augments the resource with the
   186  // standard status conditions.
   187  func Augment(u *unstructured.Unstructured) error {
   188  	res, err := Compute(u)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	conditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions")
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	if !found {
   199  		conditions = make([]interface{}, 0)
   200  	}
   201  
   202  	currentTime := time.Now().UTC().Format(time.RFC3339)
   203  
   204  	for _, resCondition := range res.Conditions {
   205  		present := false
   206  		for _, c := range conditions {
   207  			condition, ok := c.(map[string]interface{})
   208  			if !ok {
   209  				return errors.New("condition does not have the expected structure")
   210  			}
   211  			conditionType, ok := condition["type"].(string)
   212  			if !ok {
   213  				return errors.New("condition type does not have the expected type")
   214  			}
   215  			if conditionType == string(resCondition.Type) {
   216  				conditionStatus, ok := condition["status"].(string)
   217  				if !ok {
   218  					return errors.New("condition status does not have the expected type")
   219  				}
   220  				if conditionStatus != string(resCondition.Status) {
   221  					condition["lastTransitionTime"] = currentTime
   222  				}
   223  				condition["status"] = string(resCondition.Status)
   224  				condition["lastUpdateTime"] = currentTime
   225  				condition["reason"] = resCondition.Reason
   226  				condition["message"] = resCondition.Message
   227  				present = true
   228  			}
   229  		}
   230  		if !present {
   231  			conditions = append(conditions, map[string]interface{}{
   232  				"lastTransitionTime": currentTime,
   233  				"lastUpdateTime":     currentTime,
   234  				"message":            resCondition.Message,
   235  				"reason":             resCondition.Reason,
   236  				"status":             string(resCondition.Status),
   237  				"type":               string(resCondition.Type),
   238  			})
   239  		}
   240  	}
   241  	return unstructured.SetNestedSlice(u.Object, conditions, "status", "conditions")
   242  }
   243  

View as plain text