Skip to content

Commit 251b401

Browse files
authored
prep codebase for addition of integration tests (#144)
Signed-off-by: Alex Price <aprice@atlassian.com>
1 parent 8bb2fb2 commit 251b401

11 files changed

Lines changed: 134 additions & 57 deletions

File tree

cmd/manager/main.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ var (
4949
defaultCNScyclingExpiry = app.Flag("default-cns-cycling-expiry", "Fail the CNS if it has been cycling for this long").Default("3h").Duration()
5050
unhealthyPodTerminationThreshold = app.Flag("unhealthy-pod-termination-after", "How long to tolerate an un-evictable yet unhealthy pod before forcefully removing it").Default("5m").Duration()
5151

52+
cnrScaleUpWait = app.Flag("cnr-scale-up-wait", "Minimum time to wait after scaling up before checking if replacement nodes are Ready").Default("1m").Duration()
53+
cnrScaleUpLimit = app.Flag("cnr-scale-up-limit", "Maximum total time to wait for replacement nodes to come up before failing the CNR").Default("20m").Duration()
54+
cnrNodeEquilibriumWaitLimit = app.Flag("cnr-node-equilibrium-wait-limit", "Maximum time to wait for the kube-node-set and cloud-provider-instance-set to converge during the Initialised phase").Default("5m").Duration()
55+
cnrTransitionDuration = app.Flag("cnr-transition-duration", "RequeueAfter used when moving the CNR between phases").Default("10s").Duration()
56+
cnrRequeueDuration = app.Flag("cnr-requeue-duration", "RequeueAfter used while the CNR is waiting on an external condition within a phase").Default("30s").Duration()
57+
58+
cnsTransitionDuration = app.Flag("cns-transition-duration", "RequeueAfter used when moving the CNS between phases").Default("10s").Duration()
59+
cnsWaitingPodsRequeue = app.Flag("cns-waiting-pods-requeue", "RequeueAfter used while waiting for pods on the cycling node to finish naturally (Method=Wait)").Default("60s").Duration()
60+
cnsRemovingLabelsPodsRequeue = app.Flag("cns-removing-labels-pods-requeue", "RequeueAfter used while removing labels from pods on the cycling node").Default("1s").Duration()
61+
cnsDrainingRetryRequeue = app.Flag("cns-draining-retry-requeue", "RequeueAfter used when the apiserver returns 429 TooManyRequests (PDB-blocked) during drain").Default("15s").Duration()
62+
cnsDrainingPodsRequeue = app.Flag("cns-draining-pods-requeue", "RequeueAfter used while waiting for the in-flight drain to finish").Default("30s").Duration()
63+
5264
nodeControllerReconcileConcurrency = app.Flag("node-controller-reconcile-concurrency", "Maximum number of concurrent node controller reconciles").Default("1").Int()
5365
nodeControllerRequeueAfter = app.Flag("node-controller-requeue-after", "How often the node controller rechecks annotated nodes that are still covered by an active CNR").Default("5m").Duration()
5466
)
@@ -120,16 +132,26 @@ func main() {
120132

121133
// Configure the CNR transitioner options
122134
cnrOptions := cnrTransitioner.Options{
123-
DeleteCNR: *deleteCNR,
124-
DeleteCNRExpiry: *deleteCNRExpiry,
125-
DeleteCNRRequeue: *deleteCNRRequeue,
126-
HealthCheckTimeout: *healthCheckTimeout,
135+
DeleteCNR: *deleteCNR,
136+
DeleteCNRExpiry: *deleteCNRExpiry,
137+
DeleteCNRRequeue: *deleteCNRRequeue,
138+
HealthCheckTimeout: *healthCheckTimeout,
139+
ScaleUpWait: *cnrScaleUpWait,
140+
ScaleUpLimit: *cnrScaleUpLimit,
141+
NodeEquilibriumWaitLimit: *cnrNodeEquilibriumWaitLimit,
142+
TransitionDuration: *cnrTransitionDuration,
143+
RequeueDuration: *cnrRequeueDuration,
127144
}
128145

129146
// Configure the CNS transitioner options
130147
cnsOptions := cnsTransitioner.Options{
131148
DefaultCNScyclingExpiry: *defaultCNScyclingExpiry,
132149
UnhealthyPodTerminationThreshold: *unhealthyPodTerminationThreshold,
150+
TransitionDuration: *cnsTransitionDuration,
151+
WaitingPodsRequeue: *cnsWaitingPodsRequeue,
152+
RemovingLabelsPodsRequeue: *cnsRemovingLabelsPodsRequeue,
153+
DrainingRetryRequeue: *cnsDrainingRetryRequeue,
154+
DrainingPodsRequeue: *cnsDrainingPodsRequeue,
133155
}
134156

135157
// Configure the node controller options

pkg/cloudprovider/aws/builder.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import (
44
"fmt"
55

66
"github.com/atlassian-labs/cyclops/pkg/cloudprovider"
7-
fakeaws "github.com/atlassian-labs/cyclops/pkg/cloudprovider/aws/fake"
87
"github.com/aws/aws-sdk-go/aws"
98
"github.com/aws/aws-sdk-go/aws/session"
109
"github.com/aws/aws-sdk-go/service/autoscaling"
10+
"github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface"
1111
"github.com/aws/aws-sdk-go/service/ec2"
12+
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
1213
"github.com/go-logr/logr"
1314
)
1415

@@ -52,10 +53,13 @@ func NewCloudProvider(logger logr.Logger) (cloudprovider.CloudProvider, error) {
5253
return p, nil
5354
}
5455

55-
// NewGenericCloudProvider returns a new mock AWS cloud provider
56-
func NewGenericCloudProvider(autoscalingiface *fakeaws.Autoscaling, ec2iface *fakeaws.Ec2) cloudprovider.CloudProvider {
56+
// NewGenericCloudProvider returns a cloud provider built around the supplied
57+
// Autoscaling and EC2 clients. Production code uses NewCloudProvider; this
58+
// constructor lets tests inject fakes (or instrumented decorators around the
59+
// real clients).
60+
func NewGenericCloudProvider(asg autoscalingiface.AutoScalingAPI, ec2c ec2iface.EC2API) cloudprovider.CloudProvider {
5761
return &provider{
58-
autoScalingService: autoscalingiface,
59-
ec2Service: ec2iface,
62+
autoScalingService: asg,
63+
ec2Service: ec2c,
6064
}
6165
}

pkg/controller/cyclenoderequest/transitioner/integration_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestOriginalIssue_IOTimeoutCausesHealing(t *testing.T) {
3434
}
3535

3636
rm := createTestResourceManager(t, cnr, node, mockCP)
37-
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, Options{})
37+
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, defaultTestTransitionerOptions())
3838

3939
// Execute
4040
_, err := transitioner.transitionPending()
@@ -58,7 +58,7 @@ func TestOriginalIssue_IOTimeoutCausesHealing(t *testing.T) {
5858
}
5959

6060
rm := createTestResourceManager(t, cnr, node, mockCP)
61-
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, Options{})
61+
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, defaultTestTransitionerOptions())
6262

6363
// Execute
6464
result, err := transitioner.transitionPending()
@@ -139,7 +139,7 @@ func TestEndToEnd_TransientErrorRecovery(t *testing.T) {
139139
recovered := false
140140

141141
for attempt := 1; attempt <= maxAttempts; attempt++ {
142-
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, Options{})
142+
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, defaultTestTransitionerOptions())
143143
result, err := transitioner.transitionPending()
144144

145145
t.Logf("Attempt %d: Phase=%s, Requeue=%v, Error=%v",
@@ -209,7 +209,7 @@ func TestEndToEnd_PermanentErrorsStillFail(t *testing.T) {
209209
}
210210

211211
rm := createTestResourceManager(t, cnr, node, mockCP)
212-
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, Options{})
212+
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, defaultTestTransitionerOptions())
213213

214214
// Execute
215215
result, err := transitioner.transitionPending()
@@ -246,7 +246,7 @@ func TestEndToEnd_EquilibriumTimeout(t *testing.T) {
246246
}
247247

248248
rm := createTestResourceManager(t, cnr, node, mockCP)
249-
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, Options{})
249+
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, defaultTestTransitionerOptions())
250250

251251
// Execute
252252
_, err := transitioner.transitionPending()
@@ -279,7 +279,7 @@ func TestEndToEnd_CompareBeforeAndAfter(t *testing.T) {
279279
node := createTestNode("test-node-1", "aws:///us-west-2a/i-1234567890abcdef0")
280280
mockCP := &mockCloudProviderAlwaysFails{error: testError}
281281
rm := createTestResourceManager(t, cnr, node, mockCP)
282-
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, Options{})
282+
transitioner := NewCycleNodeRequestTransitioner(cnr, rm, defaultTestTransitionerOptions())
283283

284284
result, err := transitioner.transitionPending()
285285

pkg/controller/cyclenoderequest/transitioner/test_helpers.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package transitioner
22

33
import (
44
"net/http"
5+
"time"
56

67
v1 "github.com/atlassian-labs/cyclops/pkg/apis/atlassian/v1"
78
"github.com/atlassian-labs/cyclops/pkg/controller"
@@ -10,6 +11,20 @@ import (
1011
"sigs.k8s.io/controller-runtime/pkg/client"
1112
)
1213

14+
// defaultTestTransitionerOptions mirrors the production defaults declared
15+
// in cmd/manager/main.go so unit tests using NewFakeTransitioner behave
16+
// the same way as the running operator. Individual tests can replace any
17+
// field via WithTransitionerOptions.
18+
func defaultTestTransitionerOptions() Options {
19+
return Options{
20+
ScaleUpWait: 1 * time.Minute,
21+
ScaleUpLimit: 20 * time.Minute,
22+
NodeEquilibriumWaitLimit: 5 * time.Minute,
23+
TransitionDuration: 10 * time.Second,
24+
RequeueDuration: 30 * time.Second,
25+
}
26+
}
27+
1328
type Option func(t *Transitioner)
1429

1530
func WithCloudProviderInstances(nodes []*mock.Node) Option {
@@ -57,7 +72,7 @@ func NewFakeTransitioner(cnr *v1.CycleNodeRequest, opts ...Option) *Transitioner
5772
CloudProviderInstances: make([]*mock.Node, 0),
5873
KubeNodes: make([]*mock.Node, 0),
5974
extraKubeObjects: []client.Object{cnr},
60-
transitionerOptions: Options{},
75+
transitionerOptions: defaultTestTransitionerOptions(),
6176
}
6277

6378
for _, opt := range opts {

pkg/controller/cyclenoderequest/transitioner/transitioner.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,18 @@ const (
3333
// Value: "false" or missing/empty → default enabled (annotation management enabled)
3434
const nodeGroupAnnotationKey = "cyclops.atlassian.com/disable-annotation-management"
3535

36-
var (
37-
transitionDuration = 10 * time.Second
38-
requeueDuration = 30 * time.Second
39-
)
40-
4136
// CycleNodeRequestTransitioner takes a cycleNodeRequest and attempts to transition it to the next phase
4237
type CycleNodeRequestTransitioner struct {
4338
cycleNodeRequest *v1.CycleNodeRequest
4439
rm *controller.ResourceManager
4540
options Options
4641
}
4742

48-
// Options stores configurable options for the CycleNodeRequestTransitioner
43+
// Options stores configurable options for the CycleNodeRequestTransitioner.
44+
//
45+
// All fields are required to be set by the caller. The cyclops manager
46+
// (cmd/manager/main.go) provides defaults via kingpin CLI flags; tests
47+
// construct Options directly with whatever values they need.
4948
type Options struct {
5049
// DeleteCNR enables/disables deleting successful CycleNodeRequests after a certain amount of time
5150
DeleteCNR bool
@@ -58,9 +57,32 @@ type Options struct {
5857

5958
// HealthCheckTimeout controls the duration of the timeout period for health checks performed on nodes
6059
HealthCheckTimeout time.Duration
60+
61+
// ScaleUpWait is the minimum time the transitioner waits after detaching
62+
// instances before checking whether replacement Kubernetes nodes have
63+
// become Ready.
64+
ScaleUpWait time.Duration
65+
66+
// ScaleUpLimit is the maximum total time spent waiting for replacement
67+
// nodes to come up before the CNR transitions to Healing.
68+
ScaleUpLimit time.Duration
69+
70+
// NodeEquilibriumWaitLimit caps how long the transitioner will wait for
71+
// the kube-node-set and cloud-provider-instance-set to converge during
72+
// the Initialised phase.
73+
NodeEquilibriumWaitLimit time.Duration
74+
75+
// TransitionDuration is the RequeueAfter used when moving the CNR
76+
// between phases.
77+
TransitionDuration time.Duration
78+
79+
// RequeueDuration is the RequeueAfter used while the CNR is waiting on
80+
// an external condition within a phase (e.g. ScalingUp readiness,
81+
// WaitingTermination).
82+
RequeueDuration time.Duration
6183
}
6284

63-
// NewCycleNodeRequestTransitioner returns a new cycleNodeRequest transitioner
85+
// NewCycleNodeRequestTransitioner returns a new cycleNodeRequest transitioner.
6486
func NewCycleNodeRequestTransitioner(
6587
cycleNodeRequest *v1.CycleNodeRequest,
6688
rm *controller.ResourceManager,

pkg/controller/cyclenoderequest/transitioner/transitions.go

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@ import (
1818
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1919
)
2020

21-
const (
22-
scaleUpWait = 1 * time.Minute
23-
scaleUpLimit = 20 * time.Minute
24-
nodeEquilibriumWaitLimit = 5 * time.Minute
25-
)
26-
2721
// transitionUndefined transitions any CycleNodeRequests in the undefined phase to the pending phase
2822
// It checks to ensure that a valid selector has been provided.
2923
func (t *CycleNodeRequestTransitioner) transitionUndefined() (reconcile.Result, error) {
@@ -81,7 +75,7 @@ func (t *CycleNodeRequestTransitioner) transitionPending() (reconcile.Result, er
8175
// Requeue with backoff instead of transitioning to Healing
8276
return reconcile.Result{
8377
Requeue: true,
84-
RequeueAfter: requeueDuration,
78+
RequeueAfter: t.options.RequeueDuration,
8579
}, nil
8680
}
8781
// Non-retryable error, transition to Healing
@@ -123,7 +117,7 @@ func (t *CycleNodeRequestTransitioner) transitionPending() (reconcile.Result, er
123117
// After working through these attempts, requeue to run through the Pending phase from the
124118
// beginning to check the full state of nodes again. If there are any problem nodes we should
125119
// not proceed and keep requeuing until the state is fixed or the timeout has been reached.
126-
return reconcile.Result{Requeue: true, RequeueAfter: requeueDuration}, nil
120+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.RequeueDuration}, nil
127121
}
128122

129123
valid, err := t.validateInstanceState(validNodeGroupInstances)
@@ -133,7 +127,7 @@ func (t *CycleNodeRequestTransitioner) transitionPending() (reconcile.Result, er
133127

134128
if !valid {
135129
t.rm.Logger.Info("instance state not valid, requeuing")
136-
return reconcile.Result{Requeue: true, RequeueAfter: requeueDuration}, nil
130+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.RequeueDuration}, nil
137131
}
138132

139133
t.rm.Logger.Info("instance state valid, proceeding")
@@ -319,17 +313,17 @@ func (t *CycleNodeRequestTransitioner) transitionScalingUp() (reconcile.Result,
319313
scaleUpStarted := t.cycleNodeRequest.Status.ScaleUpStarted
320314

321315
// Check we have waited long enough - give the node some time to start up
322-
if time.Since(scaleUpStarted.Time) <= scaleUpWait {
316+
if time.Since(scaleUpStarted.Time) <= t.options.ScaleUpWait {
323317
t.rm.LogEvent(t.cycleNodeRequest, "ScalingUpWaiting", "Waiting for new nodes to be warmed up")
324-
return reconcile.Result{Requeue: true, RequeueAfter: scaleUpWait}, nil
318+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.ScaleUpWait}, nil
325319
}
326320

327321
nodeGroups, err := t.rm.CloudProvider.GetNodeGroups(t.cycleNodeRequest.GetNodeGroupNames())
328322
if err != nil {
329323
return t.transitionToHealing(err)
330324
}
331325
// If we have exceeded the max scale up time, then fail
332-
if scaleUpStarted.Add(scaleUpLimit).Before(time.Now()) {
326+
if scaleUpStarted.Add(t.options.ScaleUpLimit).Before(time.Now()) {
333327
return t.transitionToHealing(
334328
fmt.Errorf("all nodes failed to come up in time - instances not ready in cloud provider: %+v",
335329
nodeGroups.NotReadyInstances()))
@@ -376,7 +370,7 @@ func (t *CycleNodeRequestTransitioner) transitionScalingUp() (reconcile.Result,
376370
// also check if at least one Ready node was created after the scaleUpStarted time
377371
if !allInstancesReady || !allKubernetesNodesReady || numNodesCreatedAfterScaleUpStarted <= 0 {
378372
t.rm.LogEvent(t.cycleNodeRequest, "ScalingUpWaiting", "Waiting for new nodes to be ready")
379-
return reconcile.Result{Requeue: true, RequeueAfter: requeueDuration}, nil
373+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.RequeueDuration}, nil
380374
}
381375

382376
// Remove any nodes from the CNR object which are found to have been removed prematurely due to a race condition
@@ -404,7 +398,7 @@ func (t *CycleNodeRequestTransitioner) transitionScalingUp() (reconcile.Result,
404398
return t.transitionToHealing(err)
405399
}
406400

407-
return reconcile.Result{Requeue: true, RequeueAfter: requeueDuration}, nil
401+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.RequeueDuration}, nil
408402
}
409403
}
410404

@@ -508,7 +502,7 @@ func (t *CycleNodeRequestTransitioner) transitionCordoning() (reconcile.Result,
508502
return t.transitionToHealing(err)
509503
}
510504

511-
return reconcile.Result{Requeue: true, RequeueAfter: requeueDuration}, nil
505+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.RequeueDuration}, nil
512506
}
513507

514508
// The scale up + cordon is finished, we no longer need this list of nodes
@@ -617,7 +611,7 @@ func (t *CycleNodeRequestTransitioner) transitionFailed() (reconcile.Result, err
617611
return t.transitionToFailed(err)
618612
}
619613
if shouldRequeue {
620-
return reconcile.Result{Requeue: true, RequeueAfter: transitionDuration}, nil
614+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.TransitionDuration}, nil
621615
}
622616

623617
return reconcile.Result{}, nil
@@ -631,7 +625,7 @@ func (t *CycleNodeRequestTransitioner) transitionSuccessful() (reconcile.Result,
631625
}
632626

633627
if shouldRequeue {
634-
return reconcile.Result{Requeue: true, RequeueAfter: transitionDuration}, nil
628+
return reconcile.Result{Requeue: true, RequeueAfter: t.options.TransitionDuration}, nil
635629
}
636630

637631
// Delete failed sibling CNRs regardless of whether the CNR for the

pkg/controller/cyclenoderequest/transitioner/transitions_pending_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ func TestPendingTimeoutReached(t *testing.T) {
537537

538538
// Simulate waiting for 1s more than the wait limit
539539
cnr.Status.EquilibriumWaitStarted = &metav1.Time{
540-
Time: time.Now().Add(-nodeEquilibriumWaitLimit - time.Second),
540+
Time: time.Now().Add(-(5 * time.Minute) - time.Second),
541541
}
542542

543543
// This time should transition to the healing phase
@@ -593,7 +593,7 @@ func TestPendingReattachedCloudProviderNode(t *testing.T) {
593593

594594
// Simulate waiting for 1s less than the wait limit
595595
cnr.Status.EquilibriumWaitStarted = &metav1.Time{
596-
Time: time.Now().Add(-nodeEquilibriumWaitLimit + time.Second),
596+
Time: time.Now().Add(-(5 * time.Minute) + time.Second),
597597
}
598598

599599
_, err = fakeTransitioner.Autoscaling.AttachInstances(&autoscaling.AttachInstancesInput{
@@ -661,7 +661,7 @@ func TestPendingReattachedCloudProviderNodeTooLate(t *testing.T) {
661661

662662
// Simulate waiting for 1s more than the wait limit
663663
cnr.Status.EquilibriumWaitStarted = &metav1.Time{
664-
Time: time.Now().Add(-nodeEquilibriumWaitLimit - time.Second),
664+
Time: time.Now().Add(-(5 * time.Minute) - time.Second),
665665
}
666666

667667
_, err = fakeTransitioner.Autoscaling.AttachInstances(&autoscaling.AttachInstancesInput{

pkg/controller/cyclenoderequest/transitioner/util.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (t *CycleNodeRequestTransitioner) transitionObject(desiredPhase v1.CycleNod
113113

114114
return reconcile.Result{
115115
Requeue: true,
116-
RequeueAfter: transitionDuration,
116+
RequeueAfter: t.options.TransitionDuration,
117117
}, nil
118118
}
119119

@@ -132,7 +132,7 @@ func (t *CycleNodeRequestTransitioner) equilibriumWaitTimedOut() (bool, error) {
132132
}
133133
}
134134

135-
return time.Now().After(t.cycleNodeRequest.Status.EquilibriumWaitStarted.Add(nodeEquilibriumWaitLimit)), nil
135+
return time.Now().After(t.cycleNodeRequest.Status.EquilibriumWaitStarted.Add(t.options.NodeEquilibriumWaitLimit)), nil
136136
}
137137

138138
// reapChildren reaps CycleNodeStatus children. It returns the state that should be
@@ -481,7 +481,7 @@ func (t *CycleNodeRequestTransitioner) errorIfEquilibriumTimeoutReached() error
481481
if timedOut {
482482
return fmt.Errorf(
483483
"node count mismatch, number of kubernetes nodes does not match number of cloud provider instances after %v",
484-
nodeEquilibriumWaitLimit,
484+
t.options.NodeEquilibriumWaitLimit,
485485
)
486486
}
487487

0 commit comments

Comments
 (0)