Skip to content

Commit f69eef7

Browse files
authored
Re-iterate status evaluation. (#86)
1 parent 7aa9704 commit f69eef7

20 files changed

Lines changed: 713 additions & 164 deletions

api/v2/types_firewall.go

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package v2
22

33
import (
4+
"fmt"
45
"sort"
56
"strconv"
7+
"time"
68

79
"github.com/metal-stack/metal-lib/pkg/pointer"
810
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -185,7 +187,7 @@ const (
185187
FirewallDistanceConfigured ConditionType = "Distance"
186188
// FirewallProvisioned indicates that all health conditions have been met at least once.
187189
// Once set to true, it stays true and is used to detect condition degradation.
188-
FirewallHealthy ConditionType = "Healthy"
190+
FirewallProvisioned ConditionType = "Provisioned"
189191
)
190192

191193
// ShootAccess contains secret references to construct a shoot client in the firewall-controller to update its firewall monitor.
@@ -354,3 +356,169 @@ func SortFirewallsByImportance(fws []*Firewall) {
354356
return !a.CreationTimestamp.Before(&b.CreationTimestamp)
355357
})
356358
}
359+
360+
type (
361+
FirewallStatusResult string
362+
363+
FirewallStatusEvalResult struct {
364+
Result FirewallStatusResult
365+
Reason string
366+
TimeoutIn *time.Duration
367+
}
368+
)
369+
370+
const (
371+
FirewallStatusReady FirewallStatusResult = "ready"
372+
FirewallStatusProgressing FirewallStatusResult = "progressing"
373+
FirewallStatusUnhealthy FirewallStatusResult = "unhealthy"
374+
FirewallStatusHealthTimeout FirewallStatusResult = "health-timeout"
375+
FirewallStatusCreateTimeout FirewallStatusResult = "create-timeout"
376+
)
377+
378+
func EvaluateFirewallStatus(fw *Firewall, createTimeout, healthTimeout time.Duration) *FirewallStatusEvalResult {
379+
var (
380+
checkForTimeout = func(fw *Firewall, condition ConditionType, timeout time.Duration) (time.Duration, bool) {
381+
if timeout == 0 {
382+
return 0, false
383+
}
384+
385+
var (
386+
cond = pointer.SafeDeref(fw.Status.Conditions.Get(condition))
387+
transitionTime = cond.LastTransitionTime.Time
388+
deadline = time.Until(transitionTime.Add(timeout))
389+
)
390+
391+
if deadline < 0 {
392+
return 0, true
393+
}
394+
395+
return deadline, false
396+
}
397+
398+
collectUnhealthyConditions = func(cts ...ConditionType) []*Condition {
399+
var res []*Condition
400+
401+
for _, ct := range cts {
402+
cond := fw.Status.Conditions.Get(ct)
403+
if cond == nil {
404+
res = append(res, &Condition{Type: ct})
405+
} else if cond.Status != ConditionTrue {
406+
res = append(res, cond)
407+
}
408+
}
409+
410+
return res
411+
}
412+
413+
unhealthyTypes []string
414+
timeoutIn *time.Duration
415+
)
416+
417+
switch fw.Status.Phase {
418+
case FirewallPhaseCreating, FirewallPhaseCrashing:
419+
unhealthyConds := collectUnhealthyConditions(
420+
FirewallCreated,
421+
FirewallReady,
422+
FirewallProvisioned,
423+
)
424+
425+
if len(unhealthyConds) == 0 {
426+
return &FirewallStatusEvalResult{
427+
Result: FirewallStatusReady,
428+
Reason: "",
429+
}
430+
}
431+
432+
if createTimeout > 0 {
433+
if t, ok := checkForTimeout(fw, FirewallReady, createTimeout); ok {
434+
return &FirewallStatusEvalResult{
435+
Result: FirewallStatusCreateTimeout,
436+
Reason: fmt.Sprintf("%s create timeout exceeded, firewall not provisioned in time", createTimeout.String()),
437+
}
438+
} else if createTimeout != 0 {
439+
timeoutIn = &t
440+
}
441+
}
442+
443+
for _, c := range unhealthyConds {
444+
unhealthyTypes = append(unhealthyTypes, string(c.Type))
445+
}
446+
447+
return &FirewallStatusEvalResult{
448+
Result: FirewallStatusProgressing,
449+
Reason: fmt.Sprintf("not all health conditions are true: %v", unhealthyTypes),
450+
TimeoutIn: timeoutIn,
451+
}
452+
453+
case FirewallPhaseRunning:
454+
fallthrough
455+
456+
default:
457+
unhealthyConds := collectUnhealthyConditions(
458+
FirewallCreated,
459+
FirewallReady,
460+
FirewallProvisioned,
461+
FirewallControllerConnected,
462+
FirewallControllerSeedConnected,
463+
FirewallDistanceConfigured,
464+
)
465+
466+
if len(unhealthyConds) == 0 {
467+
return &FirewallStatusEvalResult{
468+
Result: FirewallStatusReady,
469+
Reason: "",
470+
}
471+
}
472+
473+
var (
474+
ready = pointer.SafeDeref(fw.Status.Conditions.Get(FirewallReady)).Status == ConditionTrue
475+
provisioned = pointer.SafeDeref(fw.Status.Conditions.Get(FirewallProvisioned)).Status == ConditionTrue
476+
connected = pointer.SafeDeref(fw.Status.Conditions.Get(FirewallControllerConnected)).Status == ConditionTrue
477+
seedConnected = pointer.SafeDeref(fw.Status.Conditions.Get(FirewallControllerSeedConnected)).Status == ConditionTrue
478+
)
479+
480+
if provisioned {
481+
switch {
482+
case !seedConnected:
483+
if t, ok := checkForTimeout(fw, FirewallControllerSeedConnected, healthTimeout); ok {
484+
return &FirewallStatusEvalResult{
485+
Result: FirewallStatusHealthTimeout,
486+
Reason: fmt.Sprintf("%s health timeout exceeded, seed connection lost", healthTimeout.String()),
487+
}
488+
} else if healthTimeout != 0 {
489+
timeoutIn = &t
490+
}
491+
492+
case !connected:
493+
if t, ok := checkForTimeout(fw, FirewallControllerConnected, healthTimeout); ok {
494+
return &FirewallStatusEvalResult{
495+
Result: FirewallStatusHealthTimeout,
496+
Reason: fmt.Sprintf("%s health timeout exceeded, firewall monitor not reconciled anymore", healthTimeout.String()),
497+
}
498+
} else if healthTimeout != 0 {
499+
timeoutIn = &t
500+
}
501+
502+
case !ready:
503+
if t, ok := checkForTimeout(fw, FirewallReady, healthTimeout); ok {
504+
return &FirewallStatusEvalResult{
505+
Result: FirewallStatusHealthTimeout,
506+
Reason: fmt.Sprintf("%s health timeout exceeded, firewall is not ready from perspective of the metal-api", healthTimeout.String()),
507+
}
508+
} else if healthTimeout != 0 {
509+
timeoutIn = &t
510+
}
511+
}
512+
}
513+
514+
for _, c := range unhealthyConds {
515+
unhealthyTypes = append(unhealthyTypes, string(c.Type))
516+
}
517+
518+
return &FirewallStatusEvalResult{
519+
Result: FirewallStatusUnhealthy,
520+
Reason: fmt.Sprintf("not all health conditions are true: %v", unhealthyTypes),
521+
TimeoutIn: timeoutIn,
522+
}
523+
}
524+
}

api/v2/types_firewall_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66

77
"github.com/google/go-cmp/cmp"
88
"github.com/google/go-cmp/cmp/cmpopts"
9+
"github.com/metal-stack/metal-lib/pkg/pointer"
910
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
12+
"testing/synctest"
1013
)
1114

1215
func Test_SortFirewallsByImportance(t *testing.T) {
@@ -107,3 +110,183 @@ func Test_SortFirewallsByImportance(t *testing.T) {
107110
})
108111
}
109112
}
113+
114+
func Test_EvaluateFirewallStatus(t *testing.T) {
115+
tests := []struct {
116+
name string
117+
modFn func(fw *Firewall)
118+
healthTimeout time.Duration
119+
createTimeout time.Duration
120+
want *FirewallStatusEvalResult
121+
wantReason string
122+
}{
123+
{
124+
name: "ready firewall in running phase",
125+
modFn: nil,
126+
want: &FirewallStatusEvalResult{
127+
Result: FirewallStatusReady,
128+
},
129+
},
130+
{
131+
name: "unhealthy firewall in running phase due to firewall monitor not reconciling",
132+
modFn: func(fw *Firewall) {
133+
fw.Status.Conditions.Set(Condition{
134+
Type: FirewallControllerConnected,
135+
Status: ConditionFalse,
136+
})
137+
},
138+
want: &FirewallStatusEvalResult{
139+
Result: FirewallStatusUnhealthy,
140+
Reason: "not all health conditions are true: [Connected]",
141+
},
142+
},
143+
{
144+
name: "unhealthy firewall in running phase due to firewall not reconciling",
145+
modFn: func(fw *Firewall) {
146+
fw.Status.Conditions.Set(Condition{
147+
Type: FirewallControllerSeedConnected,
148+
Status: ConditionFalse,
149+
})
150+
},
151+
want: &FirewallStatusEvalResult{
152+
Result: FirewallStatusUnhealthy,
153+
Reason: "not all health conditions are true: [SeedConnected]",
154+
},
155+
},
156+
{
157+
name: "unhealthy firewall in running phase due to readiness condition false",
158+
modFn: func(fw *Firewall) {
159+
fw.Status.Conditions.Set(Condition{
160+
Type: FirewallReady,
161+
Status: ConditionFalse,
162+
})
163+
},
164+
want: &FirewallStatusEvalResult{
165+
Result: FirewallStatusUnhealthy,
166+
Reason: "not all health conditions are true: [Ready]",
167+
},
168+
},
169+
{
170+
name: "health timeout reached because seed not connected",
171+
healthTimeout: 5 * time.Minute,
172+
modFn: func(fw *Firewall) {
173+
cond := fw.Status.Conditions.Get(FirewallControllerSeedConnected)
174+
cond.Status = ConditionFalse
175+
fw.Status.Conditions.Set(*cond)
176+
},
177+
want: &FirewallStatusEvalResult{
178+
Result: FirewallStatusHealthTimeout,
179+
Reason: "5m0s health timeout exceeded, seed connection lost",
180+
},
181+
},
182+
{
183+
name: "health timeout not yet reached",
184+
healthTimeout: 15 * time.Minute,
185+
modFn: func(fw *Firewall) {
186+
cond := fw.Status.Conditions.Get(FirewallControllerSeedConnected)
187+
cond.Status = ConditionFalse
188+
fw.Status.Conditions.Set(*cond)
189+
},
190+
want: &FirewallStatusEvalResult{
191+
Result: FirewallStatusUnhealthy,
192+
Reason: "not all health conditions are true: [SeedConnected]",
193+
TimeoutIn: pointer.Pointer(5 * time.Minute),
194+
},
195+
},
196+
{
197+
name: "create timeout reached because not provisioned",
198+
createTimeout: 5 * time.Minute,
199+
modFn: func(fw *Firewall) {
200+
fw.Status.Phase = FirewallPhaseCreating
201+
cond := fw.Status.Conditions.Get(FirewallProvisioned)
202+
cond.Status = ConditionFalse
203+
fw.Status.Conditions.Set(*cond)
204+
},
205+
want: &FirewallStatusEvalResult{
206+
Result: FirewallStatusCreateTimeout,
207+
Reason: "5m0s create timeout exceeded, firewall not provisioned in time",
208+
},
209+
},
210+
{
211+
name: "create timeout not yet reached",
212+
createTimeout: 15 * time.Minute,
213+
modFn: func(fw *Firewall) {
214+
fw.Status.Phase = FirewallPhaseCreating
215+
cond := fw.Status.Conditions.Get(FirewallProvisioned)
216+
cond.Status = ConditionFalse
217+
fw.Status.Conditions.Set(*cond)
218+
},
219+
want: &FirewallStatusEvalResult{
220+
Result: FirewallStatusProgressing,
221+
Reason: "not all health conditions are true: [Provisioned]",
222+
TimeoutIn: pointer.Pointer(5 * time.Minute),
223+
},
224+
},
225+
}
226+
for _, tt := range tests {
227+
t.Run(tt.name, func(t *testing.T) {
228+
synctest.Test(t, func(t *testing.T) {
229+
tenMinutesAgo := time.Now().Add(-10 * time.Minute)
230+
231+
fw := &Firewall{
232+
Status: FirewallStatus{
233+
Phase: FirewallPhaseRunning,
234+
Conditions: Conditions{
235+
{
236+
Type: FirewallControllerConnected,
237+
Status: ConditionTrue,
238+
LastTransitionTime: metav1.NewTime(tenMinutesAgo),
239+
LastUpdateTime: metav1.NewTime(tenMinutesAgo),
240+
},
241+
{
242+
Type: FirewallControllerSeedConnected,
243+
Status: ConditionTrue,
244+
LastTransitionTime: metav1.NewTime(tenMinutesAgo),
245+
LastUpdateTime: metav1.NewTime(tenMinutesAgo),
246+
},
247+
{
248+
Type: FirewallCreated,
249+
Status: ConditionTrue,
250+
LastTransitionTime: metav1.NewTime(tenMinutesAgo),
251+
LastUpdateTime: metav1.NewTime(tenMinutesAgo),
252+
},
253+
{
254+
Type: FirewallReady,
255+
Status: ConditionTrue,
256+
LastTransitionTime: metav1.NewTime(tenMinutesAgo),
257+
LastUpdateTime: metav1.NewTime(tenMinutesAgo),
258+
},
259+
{
260+
Type: FirewallProvisioned,
261+
Status: ConditionTrue,
262+
LastTransitionTime: metav1.NewTime(tenMinutesAgo),
263+
LastUpdateTime: metav1.NewTime(tenMinutesAgo),
264+
},
265+
{
266+
Type: FirewallDistanceConfigured,
267+
Status: ConditionTrue,
268+
LastTransitionTime: metav1.NewTime(tenMinutesAgo),
269+
LastUpdateTime: metav1.NewTime(tenMinutesAgo),
270+
},
271+
{
272+
Type: FirewallMonitorDeployed,
273+
Status: ConditionTrue,
274+
LastTransitionTime: metav1.NewTime(tenMinutesAgo),
275+
LastUpdateTime: metav1.NewTime(tenMinutesAgo),
276+
},
277+
},
278+
},
279+
}
280+
281+
if tt.modFn != nil {
282+
tt.modFn(fw)
283+
}
284+
285+
got := EvaluateFirewallStatus(fw, tt.createTimeout, tt.healthTimeout)
286+
if diff := cmp.Diff(tt.want, got); diff != "" {
287+
t.Errorf("diff = %s", diff)
288+
}
289+
})
290+
})
291+
}
292+
}

0 commit comments

Comments
 (0)