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 }