Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit bdd98e5

Browse files
committedMar 19, 2025·
Wait for Deployment rollout to succeed
1 parent 7b6b90d commit bdd98e5

File tree

4 files changed

+213
-7
lines changed

4 files changed

+213
-7
lines changed
 

‎controllers/unleash_controller.go

+41-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2121
"k8s.io/apimachinery/pkg/runtime"
2222
"k8s.io/apimachinery/pkg/types"
23+
"k8s.io/apimachinery/pkg/util/wait"
2324
"k8s.io/client-go/tools/record"
2425
ctrl "sigs.k8s.io/controller-runtime"
2526
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -34,6 +35,7 @@ import (
3435
"github.com/nais/unleasherator/pkg/o11y"
3536
"github.com/nais/unleasherator/pkg/resources"
3637
"github.com/nais/unleasherator/pkg/unleashclient"
38+
"github.com/nais/unleasherator/pkg/utils"
3739
)
3840

3941
const (
@@ -44,6 +46,9 @@ const (
4446
)
4547

4648
var (
49+
deploymentTimeout = 5 * time.Minute
50+
requeueAfter = 1 * time.Hour
51+
4752
// unleashStatus is a Prometheus metric which will be used to expose the status of the Unleash instances
4853
unleashStatus = prometheus.NewGaugeVec(
4954
prometheus.GaugeOpts{
@@ -299,15 +304,30 @@ func (r *UnleashReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
299304
return ctrl.Result{}, err
300305
}
301306

307+
// Wait for Deployment rollout to finish before testing connection This is to
308+
// avoid testing connection to the previous instance if the Deployment is not
309+
// ready yet. Delay requeue to avoid tying up the reconciler since waiting is
310+
// done in the same reconcile loop.
311+
log.Info("Waiting for Deployment rollout to finish")
312+
err = r.waitForDeployment(ctx, deploymentTimeout, req.NamespacedName)
313+
if err != nil {
314+
if statusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Deployment rollout timed out"); statusErr != nil {
315+
return ctrl.Result{RequeueAfter: deploymentTimeout}, statusErr
316+
}
317+
return ctrl.Result{RequeueAfter: deploymentTimeout}, err
318+
}
319+
320+
// Set the reconcile status of the Unleash instance to available
321+
log.Info("Successfully reconciled Unleash resources")
302322
if err = r.updateStatusReconcileSuccess(ctx, unleash); err != nil {
303323
return ctrl.Result{}, err
304324
}
305325

306326
span.AddEvent("Testing connection to Unleash instance")
307327
stats, err := r.testConnection(unleash, ctx, log)
308328
if err != nil {
309-
if err := r.updateStatusConnectionFailed(ctx, unleash, err, "Failed to connect to Unleash instance"); err != nil {
310-
return ctrl.Result{}, err
329+
if statusErr := r.updateStatusConnectionFailed(ctx, unleash, err, "Failed to connect to Unleash instance"); statusErr != nil {
330+
return ctrl.Result{}, statusErr
311331
}
312332

313333
return ctrl.Result{}, err
@@ -322,13 +342,12 @@ func (r *UnleashReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
322342
}
323343

324344
// Publish the Unleash instance to federation if enabled
325-
err = r.publish(ctx, unleash)
326-
if err != nil {
345+
if err = r.publish(ctx, unleash); err != nil {
327346
return ctrl.Result{}, err
328347
}
329348

330349
log.Info("Reconciliation of Unleash finished")
331-
return ctrl.Result{RequeueAfter: 1 * time.Hour}, nil
350+
return ctrl.Result{RequeueAfter: requeueAfter}, nil
332351
}
333352

334353
// publish the Unleash instance to pubsub if federation is enabled.
@@ -561,8 +580,6 @@ func (r *UnleashReconciler) reconcileIngress(ctx context.Context, unleash *unlea
561580
return ctrl.Result{}, nil
562581
}
563582

564-
// update prometheus metrics
565-
566583
return ctrl.Result{}, nil
567584
}
568585

@@ -710,6 +727,23 @@ func (r *UnleashReconciler) reconcileService(ctx context.Context, unleash *unlea
710727
return ctrl.Result{}, nil
711728
}
712729

730+
// waitForDeployment will wait for the deployment to be available
731+
func (r *UnleashReconciler) waitForDeployment(ctx context.Context, timeout time.Duration, key types.NamespacedName) error {
732+
ctx, cancel := context.WithTimeout(ctx, timeout)
733+
defer cancel()
734+
735+
err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) {
736+
deployment := &appsv1.Deployment{}
737+
if err := r.Client.Get(ctx, key, deployment); err != nil {
738+
return false, err
739+
}
740+
741+
return utils.DeploymentIsReady(deployment), nil
742+
})
743+
744+
return err
745+
}
746+
713747
// testConnection will test the connection to the Unleash instance
714748
func (r *UnleashReconciler) testConnection(unleash resources.UnleashInstance, ctx context.Context, log logr.Logger) (*unleashclient.InstanceAdminStatsResult, error) {
715749
client, err := unleash.ApiClient(ctx, r.Client, r.OperatorNamespace)

‎controllers/unleash_controller_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,47 @@ func getUnleash(k8sClient client.Client, ctx context.Context, unleash *unleashv1
3232
return unsetConditionLastTransitionTime(unleash.Status.Conditions), nil
3333
}
3434

35+
func getDeployment(k8sClient client.Client, ctx context.Context, namespacedName client.ObjectKey, deployment *appsv1.Deployment) error {
36+
return k8sClient.Get(ctx, namespacedName, deployment)
37+
}
38+
39+
func setDeploymentStatusFailed(deployment *appsv1.Deployment) {
40+
deployment.Status.Conditions = []appsv1.DeploymentCondition{
41+
{
42+
Type: appsv1.DeploymentProgressing,
43+
Status: corev1.ConditionFalse,
44+
Reason: "ProgressDeadlineExceeded",
45+
Message: `Progress deadline exceeded.`,
46+
LastUpdateTime: metav1.Time{
47+
Time: time.Now(),
48+
},
49+
LastTransitionTime: metav1.Time{
50+
Time: time.Now(),
51+
},
52+
},
53+
}
54+
}
55+
56+
func setDeploymentStatusAvailable(deployment *appsv1.Deployment) {
57+
deployment.Status.Conditions = []appsv1.DeploymentCondition{
58+
{
59+
Type: appsv1.DeploymentProgressing,
60+
Status: corev1.ConditionTrue,
61+
Reason: "NewReplicaSetAvailable",
62+
Message: `ReplicaSet "fake-abc123" has successfully progressed.`,
63+
LastUpdateTime: metav1.Time{
64+
Time: time.Now(),
65+
},
66+
LastTransitionTime: metav1.Time{
67+
Time: time.Now(),
68+
},
69+
},
70+
}
71+
}
72+
3573
var _ = Describe("Unleash controller", func() {
74+
deploymentTimeout = time.Second * 1
75+
3676
const (
3777
UnleashNamespace = "default"
3878
UnleashVersion = "v5.1.2"
@@ -140,6 +180,40 @@ var _ = Describe("Unleash controller", func() {
140180
Expect(k8sClient.Delete(ctx, createdUnleash)).Should(Succeed())
141181
})
142182

183+
It("Should fail if Deployment rollout is not complete", func() {
184+
ctx := context.Background()
185+
186+
By("By creating a new Unleash")
187+
unleash := unleashResource("test-unleash-rollout-fail", UnleashNamespace, unleashv1.UnleashSpec{
188+
189+
Database: unleashv1.UnleashDatabaseConfig{
190+
URL: "postgres://unleash:unleash@unleash-postgres:5432/unleash?ssl=false",
191+
},
192+
})
193+
Expect(k8sClient.Create(ctx, unleash)).Should(Succeed())
194+
195+
By("By faking Deployment status as failed")
196+
createdDeployment := &appsv1.Deployment{}
197+
Eventually(getDeployment, timeout, interval).WithArguments(k8sClient, ctx, unleash.NamespacedName(), createdDeployment).Should(Succeed())
198+
setDeploymentStatusFailed(createdDeployment)
199+
Expect(k8sClient.Status().Update(ctx, createdDeployment)).Should(Succeed())
200+
201+
By("By checking that Unleash is failed")
202+
createdUnleash := &unleashv1.Unleash{ObjectMeta: unleash.ObjectMeta}
203+
Eventually(getUnleash, timeout, interval).WithArguments(k8sClient, ctx, createdUnleash).Should(ContainElement(metav1.Condition{
204+
Type: unleashv1.UnleashStatusConditionTypeReconciled,
205+
Status: metav1.ConditionFalse,
206+
Reason: "Reconciling",
207+
Message: "Deployment rollout timed out",
208+
}))
209+
Expect(createdUnleash.IsReady()).To(BeFalse())
210+
Expect(createdUnleash.Status.Reconciled).To(BeFalse())
211+
Expect(createdUnleash.Status.Connected).To(BeFalse())
212+
213+
By("By cleaning up the Unleash")
214+
Expect(k8sClient.Delete(ctx, createdUnleash)).Should(Succeed())
215+
})
216+
143217
PIt("Should fail when it cannot connect to Unleash")
144218

145219
It("Should succeed when it can connect to Unleash", func() {
@@ -153,6 +227,13 @@ var _ = Describe("Unleash controller", func() {
153227
})
154228
Expect(k8sClient.Create(ctx, unleash)).Should(Succeed())
155229

230+
By("By faking Deployment status as available")
231+
createdDeployment := &appsv1.Deployment{}
232+
Eventually(getDeployment, timeout, interval).WithArguments(k8sClient, ctx, unleash.NamespacedName(), createdDeployment).Should(Succeed())
233+
setDeploymentStatusAvailable(createdDeployment)
234+
Expect(k8sClient.Status().Update(ctx, createdDeployment)).Should(Succeed())
235+
236+
By("By checking that Unleash is connected")
156237
createdUnleash := &unleashv1.Unleash{ObjectMeta: unleash.ObjectMeta}
157238
Eventually(getUnleash, timeout, interval).WithArguments(k8sClient, ctx, createdUnleash).Should(ContainElement(metav1.Condition{
158239
Type: unleashv1.UnleashStatusConditionTypeConnected,
@@ -221,6 +302,13 @@ var _ = Describe("Unleash controller", func() {
221302
})
222303
Expect(k8sClient.Create(ctx, unleash)).Should(Succeed())
223304

305+
By("By faking Deployment status as available")
306+
createdDeployment := &appsv1.Deployment{}
307+
Eventually(getDeployment, timeout, interval).WithArguments(k8sClient, ctx, unleash.NamespacedName(), createdDeployment).Should(Succeed())
308+
setDeploymentStatusAvailable(createdDeployment)
309+
Expect(k8sClient.Status().Update(ctx, createdDeployment)).Should(Succeed())
310+
311+
By("By checking that Unleash is connected")
224312
createdUnleash := &unleashv1.Unleash{ObjectMeta: unleash.ObjectMeta}
225313
Eventually(getUnleash, timeout, interval).WithArguments(k8sClient, ctx, createdUnleash).Should(ContainElement(metav1.Condition{
226314
Type: unleashv1.UnleashStatusConditionTypeConnected,

‎pkg/utils/k8s.go

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"sync"
66

7+
appsv1 "k8s.io/api/apps/v1"
78
corev1 "k8s.io/api/core/v1"
89
"sigs.k8s.io/controller-runtime/pkg/client"
910

@@ -33,6 +34,16 @@ func SecretEnvVar(name, secretName, secretKey string) corev1.EnvVar {
3334
}
3435
}
3536

37+
// DeploymentIsReady returns true if the rollout of the given deployment has completed successfully.
38+
func DeploymentIsReady(deployment *appsv1.Deployment) bool {
39+
for _, condition := range deployment.Status.Conditions {
40+
if condition.Type == appsv1.DeploymentProgressing && condition.Status == corev1.ConditionTrue && condition.Reason == "NewReplicaSetAvailable" {
41+
return true
42+
}
43+
}
44+
return false
45+
}
46+
3647
// UpsertObject upserts the given object in Kubernetes. If the object already exists, it is updated.
3748
// If the object does not exist, it is created. The object is identified by its key, which is extracted
3849
// from the object itself. The function returns an error if the upsert operation fails.

‎pkg/utils/k8s_test.go

+73
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"reflect"
66
"testing"
77

8+
appsv1 "k8s.io/api/apps/v1"
89
corev1 "k8s.io/api/core/v1"
910
)
1011

@@ -104,3 +105,75 @@ func TestSecretEnvVar(t *testing.T) {
104105
})
105106
}
106107
}
108+
109+
func TestDeploymentIsReady(t *testing.T) {
110+
tests := []struct {
111+
name string
112+
deployment *appsv1.Deployment
113+
expected bool
114+
}{
115+
{
116+
name: "Deployment is ready",
117+
deployment: &appsv1.Deployment{
118+
Status: appsv1.DeploymentStatus{
119+
Conditions: []appsv1.DeploymentCondition{
120+
{
121+
Type: appsv1.DeploymentProgressing,
122+
Status: corev1.ConditionTrue,
123+
Reason: "NewReplicaSetAvailable",
124+
},
125+
},
126+
},
127+
},
128+
expected: true,
129+
},
130+
{
131+
name: "Deployment is not ready",
132+
deployment: &appsv1.Deployment{
133+
Status: appsv1.DeploymentStatus{
134+
Conditions: []appsv1.DeploymentCondition{
135+
{
136+
Type: appsv1.DeploymentProgressing,
137+
Status: corev1.ConditionTrue,
138+
Reason: "NewReplicaSetCreated",
139+
},
140+
},
141+
},
142+
},
143+
expected: false,
144+
},
145+
{
146+
name: "Deployment failed",
147+
deployment: &appsv1.Deployment{
148+
Status: appsv1.DeploymentStatus{
149+
Conditions: []appsv1.DeploymentCondition{
150+
{
151+
Type: appsv1.DeploymentProgressing,
152+
Status: corev1.ConditionFalse,
153+
Reason: "ProgressDeadlineExceeded",
154+
},
155+
},
156+
},
157+
},
158+
expected: false,
159+
},
160+
{
161+
name: "No conditions",
162+
deployment: &appsv1.Deployment{
163+
Status: appsv1.DeploymentStatus{
164+
Conditions: []appsv1.DeploymentCondition{},
165+
},
166+
},
167+
expected: false,
168+
},
169+
}
170+
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
got := DeploymentIsReady(tt.deployment)
174+
if got != tt.expected {
175+
t.Errorf("DeploymentIsReady() = %v, want %v", got, tt.expected)
176+
}
177+
})
178+
}
179+
}

0 commit comments

Comments
 (0)
Please sign in to comment.