-
Notifications
You must be signed in to change notification settings - Fork 104
/
instance_types_helpers.go
319 lines (276 loc) · 10.2 KB
/
instance_types_helpers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
package v1beta1
import (
"context"
"fmt"
"log"
"github.com/thoas/go-funk"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
instanceCleanupFinalizerName = "kudo.dev.instance.cleanup"
)
// ScheduledPlan returns plan status of currently active plan or nil if no plan is running. In most cases this method
// will return the same plan status as the [GetPlanInProgress](pkg/apis/kudo/v1beta1/instance_types_helpers.go:25) below.
// However, there is a small window where both might return different results:
// 1. GetScheduledPlan reads the plan from i.Spec.PlanExecution.PlanName which is set and reset by the instance admission
// webhook
// 2. GetPlanInProgress goes through i.Spec.PlanStatus map and returns the first found plan that is running
//
// In (1), i.Spec.PlanExecution.PlanName is set directly when the user updates the instance and reset **after** the plan
// is terminal
// In (2) i.Spec.PlanStatus is updated **AFTER** the instance controller is done with the reconciliation call
func (i *Instance) GetScheduledPlan() *PlanStatus {
return i.PlanStatus(i.Spec.PlanExecution.PlanName)
}
// GetPlanInProgress returns plan status of currently active plan or nil if no plan is running
func (i *Instance) GetPlanInProgress() *PlanStatus {
for _, p := range i.Status.PlanStatus {
if p.Status.IsRunning() {
return &p
}
}
return nil
}
// GetLastExecutedPlanStatus returns status of plan that is currently running, if there is one running
// if no plan is running it looks for last executed plan based on timestamps
func (i *Instance) GetLastExecutedPlanStatus() *PlanStatus {
if i.NoPlanEverExecuted() {
return nil
}
activePlan := i.GetPlanInProgress()
if activePlan != nil {
return activePlan
}
var lastExecutedPlan *PlanStatus
for n := range i.Status.PlanStatus {
p := i.Status.PlanStatus[n]
if p.Status == ExecutionNeverRun {
continue // only interested in plans that run
}
if lastExecutedPlan == nil {
lastExecutedPlan = &p // first plan that was run and we're iterating over
} else if wasRunAfter(p, *lastExecutedPlan) {
lastExecutedPlan = &p // this plan was run after the plan we have chosen before
}
}
return lastExecutedPlan
}
// NoPlanEverExecuted returns true is this is new instance for which we never executed any plan
func (i *Instance) NoPlanEverExecuted() bool {
for _, p := range i.Status.PlanStatus {
if p.Status != ExecutionNeverRun {
return false
}
}
return true
}
// UpdateInstanceStatus updates `Status.PlanStatus` and `Status.AggregatedStatus` property based on the given plan
func (i *Instance) UpdateInstanceStatus(ps *PlanStatus, updatedTimestamp *metav1.Time) {
for k, v := range i.Status.PlanStatus {
if v.Name == ps.Name {
ps.LastUpdatedTimestamp = updatedTimestamp
i.Status.PlanStatus[k] = *ps
i.Spec.PlanExecution.Status = ps.Status
}
}
}
// ResetPlanStatus method resets a PlanStatus for a passed plan name and instance. Plan/phase/step statuses
// are set to ExecutionPending meaning that the controller will restart plan execution.
func (i *Instance) ResetPlanStatus(ps *PlanStatus, uid types.UID, updatedTimestamp *metav1.Time) {
ps.UID = uid
ps.Status = ExecutionPending
for i := range ps.Phases {
ps.Phases[i].Set(ExecutionPending)
for j := range ps.Phases[i].Steps {
ps.Phases[i].Steps[j].Set(ExecutionPending)
}
}
// update plan status and instance aggregated status
i.UpdateInstanceStatus(ps, updatedTimestamp)
}
// IsDeleting returns true is the instance is being deleted.
func (i *Instance) IsDeleting() bool {
// a delete request is indicated by a non-zero 'metadata.deletionTimestamp',
// see https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers
return !i.ObjectMeta.DeletionTimestamp.IsZero()
}
func (i *Instance) HasNoFinalizers() bool { return len(i.GetFinalizers()) == 0 }
// OperatorVersionNamespace returns the namespace of the OperatorVersion that the Instance references.
func (i *Instance) OperatorVersionNamespace() string {
if i.Spec.OperatorVersion.Namespace == "" {
return i.ObjectMeta.Namespace
}
return i.Spec.OperatorVersion.Namespace
}
func (i *Instance) PlanStatus(plan string) *PlanStatus {
for _, planStatus := range i.Status.PlanStatus {
if planStatus.Name == plan {
return &planStatus
}
}
return nil
}
func (i *Instance) HasCleanupFinalizer() bool {
return funk.ContainsString(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
}
// TryAddFinalizer adds the cleanup finalizer to an instance if the finalizer
// hasn't been added yet, the instance has a cleanup plan and the cleanup plan
// didn't run yet. Returns true if the cleanup finalizer has been added.
func (i *Instance) TryAddFinalizer() bool {
if !i.HasCleanupFinalizer() {
planStatus := i.PlanStatus(CleanupPlanName)
// avoid adding a finalizer multiple times: we only add it if the corresponding
// plan.Status is nil (meaning the plan never ran) or if it exists but equals ExecutionNeverRun
if planStatus == nil || planStatus.Status == ExecutionNeverRun {
i.ObjectMeta.Finalizers = append(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
return true
}
}
return false
}
// TryRemoveFinalizer removes the cleanup finalizer of an instance if it has
// been added, the instance has a cleanup plan and the cleanup plan *successfully* finished.
// Returns true if the cleanup finalizer has been removed.
func (i *Instance) TryRemoveFinalizer() bool {
if funk.ContainsString(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName) {
if planStatus := i.PlanStatus(CleanupPlanName); planStatus != nil {
// we check IsFinished and *not* IsTerminal here so that the finalizer is not removed in the FatalError
// case. This way a human operator has to intervene and we don't leave garbage in the cluster.
if planStatus.Status.IsFinished() {
log.Printf("Removing finalizer on instance %s/%s, cleanup plan is finished", i.Namespace, i.Name)
i.ObjectMeta.Finalizers = remove(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
return true
}
} else {
// We have a finalizer but no cleanup plan. This could be due to an updated instance.
// Let's remove the finalizer.
log.Printf("Removing finalizer on instance %s/%s because there is no cleanup plan", i.Namespace, i.Name)
i.ObjectMeta.Finalizers = remove(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
return true
}
}
return false
}
func remove(values []string, s string) []string {
return funk.FilterString(values, func(str string) bool {
return str != s
})
}
// GetOperatorVersion retrieves OperatorVersion belonging to the given instance
func (i *Instance) GetOperatorVersion(c client.Reader) (ov *OperatorVersion, err error) {
return GetOperatorVersionByName(i.Spec.OperatorVersion.Name, i.OperatorVersionNamespace(), c)
}
func GetOperatorVersionByName(ovn, ns string, c client.Reader) (ov *OperatorVersion, err error) {
ov = &OperatorVersion{}
err = c.Get(context.TODO(),
types.NamespacedName{
Name: ovn,
Namespace: ns,
},
ov)
if err != nil {
return nil, err
}
return ov, nil
}
// wasRunAfter returns true if p1 was run after p2
func wasRunAfter(p1 PlanStatus, p2 PlanStatus) bool {
if p1.Status == ExecutionNeverRun || p2.Status == ExecutionNeverRun || p1.LastUpdatedTimestamp == nil || p2.LastUpdatedTimestamp == nil {
return false
}
return p1.LastUpdatedTimestamp.Time.After(p2.LastUpdatedTimestamp.Time)
}
// GetExistingParamDefinitions retrieves parameter metadata from OperatorVersion
func GetExistingParamDefinitions(params map[string]string, ov *OperatorVersion) []Parameter {
defs := []Parameter{}
for p1 := range params {
for _, p2 := range ov.Spec.Parameters {
if p2.Name == p1 {
defs = append(defs, p2)
}
}
}
return defs
}
// GetParamDefinitions retrieves parameter metadata from OperatorVersion but returns an error if any parameter is missing
func GetParamDefinitions(params map[string]string, ov *OperatorVersion) ([]Parameter, error) {
defs := []Parameter{}
for p1 := range params {
p1 := p1
p2 := funk.Find(ov.Spec.Parameters, func(e Parameter) bool {
return e.Name == p1
})
if p2 == nil {
return nil, fmt.Errorf("failed to find parameter %q in the OperatorVersion", p1)
}
defs = append(defs, p2.(Parameter))
}
return defs, nil
}
// ParameterDiff returns map containing all parameters that were removed or changed between old and new
func ParameterDiff(old, new map[string]string) map[string]string {
diff := make(map[string]string)
for key, val := range old {
// If a parameter was removed in the new spec
if _, ok := new[key]; !ok {
diff[key] = val
}
}
for key, val := range new {
// If new spec parameter was added or changed
if v, ok := old[key]; !ok || v != val {
diff[key] = val
}
}
return diff
}
// RichParameterDiff compares new and old map and returns two maps: first containing all changed/added
// and second all removed parameters.
func RichParameterDiff(old, new map[string]string) (changed, removed map[string]string) {
changed, removed = make(map[string]string), make(map[string]string)
for key, val := range old {
// If a parameter was removed in the new spec
if _, ok := new[key]; !ok {
removed[key] = val
}
}
for key, val := range new {
// If new spec parameter was added or changed
if v, ok := old[key]; !ok || v != val {
changed[key] = val
}
}
return
}
func CleanupPlanExists(ov *OperatorVersion) bool { return PlanExists(CleanupPlanName, ov) }
func PlanExists(plan string, ov *OperatorVersion) bool {
_, ok := ov.Spec.Plans[plan]
return ok
}
// SelectPlan returns nil if none of the plan exists, otherwise the first one in list that exists
func SelectPlan(possiblePlans []string, ov *OperatorVersion) *string {
for _, plan := range possiblePlans {
if _, ok := ov.Spec.Plans[plan]; ok {
return &plan
}
}
return nil
}
func GetStepStatus(stepName string, phaseStatus *PhaseStatus) *StepStatus {
for i, p := range phaseStatus.Steps {
if p.Name == stepName {
return &phaseStatus.Steps[i]
}
}
return nil
}
func GetPhaseStatus(phaseName string, planStatus *PlanStatus) *PhaseStatus {
for i, p := range planStatus.Phases {
if p.Name == phaseName {
return &planStatus.Phases[i]
}
}
return nil
}