github.com/iter8-tools/iter8@v1.1.2/base/readiness.go (about)

     1  package base
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	log "github.com/iter8-tools/iter8/base/log"
    12  
    13  	corev1 "k8s.io/api/core/v1"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    16  	"k8s.io/apimachinery/pkg/runtime/schema"
    17  	"k8s.io/apimachinery/pkg/util/wait"
    18  	"k8s.io/client-go/rest"
    19  	"k8s.io/client-go/util/retry"
    20  )
    21  
    22  const (
    23  	// ReadinessTaskName is the task name
    24  	ReadinessTaskName = "ready"
    25  
    26  	// defaultTimeout is default timeout for readiness command
    27  	defaultTimeout = "10s"
    28  )
    29  
    30  // ReadinessInputs identifies the K8s object to test for existence and
    31  // the (optional) condition that should be tested (succeeds if true).
    32  type readinessInputs struct {
    33  	// Group of the object. Optional. If unspecified it will be defaulted to ""
    34  	Group string `json:"group,omitempty" yaml:"group,omitempty"`
    35  	// Version of the object. Optional. If unspecified it will be defaulted to ""
    36  	Version string `json:"version,omitempty" yaml:"version,omitempty"`
    37  	// Resource type of the object. Required.
    38  	Resource string `json:"resource" yaml:"resource"`
    39  	// Namespace of the object. Optional. If left unspecified, this will be defaulted to the namespace of the experiment
    40  	Namespace *string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
    41  	// Name of the object
    42  	Name string `json:"name" yaml:"name"`
    43  	// Conditions is list of conditions to check for value of "True"
    44  	Conditions []string `json:"conditions" yaml:"conditions"`
    45  	// Timeout is maximum time spent trying to find object and check condition
    46  	Timeout *string `json:"timeout" yaml:"timeout"`
    47  }
    48  
    49  // ReadinessTask checks existence and readiness of specified resources
    50  type readinessTask struct {
    51  	TaskMeta
    52  	With readinessInputs `json:"with" yaml:"with"`
    53  }
    54  
    55  // initializeDefaults sets default values for the readiness task
    56  func (t *readinessTask) initializeDefaults() {
    57  	if t.With.Timeout == nil {
    58  		t.With.Timeout = StringPointer(defaultTimeout)
    59  	}
    60  
    61  	// set Namespace (from context) if not already set
    62  	if t.With.Namespace == nil {
    63  		t.With.Namespace = StringPointer(kd.Namespace())
    64  	}
    65  }
    66  
    67  // validateInputs validates task inputs
    68  // at present all validation at initialization
    69  func (t *readinessTask) validateInputs() error {
    70  	return nil
    71  }
    72  
    73  // run executes the task
    74  func (t *readinessTask) run(_ *Experiment) error {
    75  	// validation
    76  	err := t.validateInputs()
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	// kd is required by initializeDefaults
    82  	if err = kd.initKube(); err != nil {
    83  		return err
    84  	}
    85  	// initialize default values
    86  	t.initializeDefaults()
    87  
    88  	// parse timeout
    89  	timeout, err := time.ParseDuration(*t.With.Timeout)
    90  	if err != nil {
    91  		e := errors.New("invalid format for timeout")
    92  		log.Logger.WithStackTrace(err.Error()).Error(e)
    93  		return e
    94  	}
    95  
    96  	// get rest config
    97  	restConfig, err := kd.EnvSettings.RESTClientGetter().ToRESTConfig()
    98  	if err != nil {
    99  		e := errors.New("unable to get Kubernetes REST config")
   100  		log.Logger.WithStackTrace(err.Error()).Error(e)
   101  		return e
   102  	}
   103  
   104  	// do the work: check for object and condition
   105  	// repeat until time out
   106  	interval := 1 * time.Second
   107  	err = retry.OnError(
   108  		wait.Backoff{
   109  			Steps:    int(timeout / interval),
   110  			Cap:      timeout,
   111  			Duration: interval,
   112  			Factor:   1.0,
   113  			Jitter:   0.1,
   114  		},
   115  		func(err error) bool {
   116  			log.Logger.Error(err)
   117  			return true
   118  		}, // retry on all failures
   119  		func() error {
   120  			return checkObjectExistsAndConditionTrue(t, restConfig)
   121  		},
   122  	)
   123  	return err
   124  }
   125  
   126  // checkObjectExistsAndConditionTrue determines if the object exists
   127  // if so, it further checks if the requested condition is "True"
   128  func checkObjectExistsAndConditionTrue(t *readinessTask, _ *rest.Config) error {
   129  	log.Logger.Trace("looking for resource (", t.With.Group, "/", t.With.Version, ") ", t.With.Resource, ": ", t.With.Name, " in namespace ", *t.With.Namespace)
   130  
   131  	obj, err := kd.dynamicClient.Resource(gvr(&t.With)).Namespace(*t.With.Namespace).Get(context.Background(), t.With.Name, metav1.GetOptions{})
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	// if no conditios to check were specified, we can return now
   137  	if len(t.With.Conditions) == 0 {
   138  		return nil
   139  	}
   140  
   141  	// set err to nil; will set if there is a problem finding conditions
   142  	err = nil
   143  	var cs *string
   144  	for _, condition := range t.With.Conditions {
   145  		// otherwise, find the condition and check that it is "True"
   146  		log.Logger.Trace("looking for condition: ", condition)
   147  
   148  		cs, err = getConditionStatus(obj, condition)
   149  		if err != nil {
   150  			continue
   151  		}
   152  		if strings.EqualFold(*cs, string(corev1.ConditionTrue)) {
   153  			return nil
   154  		}
   155  		err = errors.New("condition status not True")
   156  	}
   157  	return err
   158  }
   159  
   160  func gvr(objRef *readinessInputs) schema.GroupVersionResource {
   161  	return schema.GroupVersionResource{
   162  		Group:    objRef.Group,
   163  		Version:  objRef.Version,
   164  		Resource: objRef.Resource,
   165  	}
   166  }
   167  
   168  func getConditionStatus(obj *unstructured.Unstructured, conditionType string) (*string, error) {
   169  	if obj == nil {
   170  		return nil, errors.New("no object")
   171  	}
   172  
   173  	resultJSON, err := obj.MarshalJSON()
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	resultObj := make(map[string]interface{})
   179  	err = json.Unmarshal(resultJSON, &resultObj)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	// get object status
   185  	objStatusInterface, ok := resultObj["status"]
   186  	if !ok {
   187  		return nil, errors.New("object does not contain a status")
   188  	}
   189  	objStatus := objStatusInterface.(map[string]interface{})
   190  
   191  	conditionsInterface, ok := objStatus["conditions"]
   192  	if !ok {
   193  		return nil, errors.New("object status does not contain conditions")
   194  	}
   195  	conditions := conditionsInterface.([]interface{})
   196  	for _, conditionInterface := range conditions {
   197  		condition := conditionInterface.(map[string]interface{})
   198  		cTypeInterface, ok := condition["type"]
   199  		if !ok {
   200  			return nil, errors.New("condition does not have a type")
   201  		}
   202  		cType := cTypeInterface.(string)
   203  		if strings.EqualFold(cType, conditionType) {
   204  			conditionStatusInterface, ok := condition["status"]
   205  			if !ok {
   206  				return nil, fmt.Errorf("condition %s does not have a value", cType)
   207  			}
   208  			conditionStatus := conditionStatusInterface.(string)
   209  			return StringPointer(conditionStatus), nil
   210  		}
   211  	}
   212  	return nil, errors.New("expected condition not found")
   213  }