Skip to content

Commit db4c23b

Browse files
committed
Wait for Deployment rollout to succeed
1 parent dc2630d commit db4c23b

File tree

4 files changed

+229
-25
lines changed

4 files changed

+229
-25
lines changed

controllers/unleash_controller.go

+57-25
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1818
"k8s.io/apimachinery/pkg/runtime"
1919
"k8s.io/apimachinery/pkg/types"
20+
"k8s.io/apimachinery/pkg/util/wait"
2021
"k8s.io/client-go/tools/record"
2122
ctrl "sigs.k8s.io/controller-runtime"
2223
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -30,6 +31,7 @@ import (
3031
"github.com/nais/unleasherator/pkg/federation"
3132
"github.com/nais/unleasherator/pkg/resources"
3233
"github.com/nais/unleasherator/pkg/unleashclient"
34+
"github.com/nais/unleasherator/pkg/utils"
3335
)
3436

3537
const (
@@ -40,6 +42,9 @@ const (
4042
)
4143

4244
var (
45+
deploymentTimeout = 5 * time.Minute
46+
requeueAfter = 1 * time.Hour
47+
4348
// unleashStatus is a Prometheus metric which will be used to expose the status of the Unleash instances
4449
unleashStatus = prometheus.NewGaugeVec(
4550
prometheus.GaugeOpts{
@@ -205,38 +210,38 @@ func (r *UnleashReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
205210

206211
res, err = r.reconcileSecrets(ctx, unleash)
207212
if err != nil {
208-
if err := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Secrets"); err != nil {
209-
return ctrl.Result{}, err
213+
if statsusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Secrets"); statsusErr != nil {
214+
return ctrl.Result{}, statsusErr
210215
}
211216
return ctrl.Result{}, err
212217
} else if res.Requeue {
213218
return res, nil
214219
}
215220

216-
res, err = r.reconcileDeployment(ctx, unleash)
221+
res, err = r.reconcileNetworkPolicy(ctx, unleash)
217222
if err != nil {
218-
if err := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Deployment"); err != nil {
219-
return ctrl.Result{}, err
223+
if statsusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile NetworkPolicy"); statsusErr != nil {
224+
return ctrl.Result{}, statsusErr
220225
}
221226
return ctrl.Result{}, err
222227
} else if res.Requeue {
223228
return res, nil
224229
}
225230

226-
res, err = r.reconcileService(ctx, unleash)
231+
res, err = r.reconcileDeployment(ctx, unleash)
227232
if err != nil {
228-
if err := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Service"); err != nil {
229-
return ctrl.Result{}, err
233+
if statsusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Deployment"); statsusErr != nil {
234+
return ctrl.Result{}, statsusErr
230235
}
231236
return ctrl.Result{}, err
232237
} else if res.Requeue {
233238
return res, nil
234239
}
235240

236-
res, err = r.reconcileNetworkPolicy(ctx, unleash)
241+
res, err = r.reconcileService(ctx, unleash)
237242
if err != nil {
238-
if err := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile NetworkPolicy"); err != nil {
239-
return ctrl.Result{}, err
243+
if statsusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Service"); statsusErr != nil {
244+
return ctrl.Result{}, statsusErr
240245
}
241246
return ctrl.Result{}, err
242247
} else if res.Requeue {
@@ -245,8 +250,8 @@ func (r *UnleashReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
245250

246251
res, err = r.reconcileIngresses(ctx, unleash)
247252
if err != nil {
248-
if err := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Ingresses"); err != nil {
249-
return ctrl.Result{}, err
253+
if statsusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile Ingresses"); statsusErr != nil {
254+
return ctrl.Result{}, statsusErr
250255
}
251256
return ctrl.Result{}, err
252257
} else if res.Requeue {
@@ -255,8 +260,8 @@ func (r *UnleashReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
255260

256261
res, err = r.reconcileServiceMonitor(ctx, unleash)
257262
if err != nil {
258-
if err := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile ServiceMonitor"); err != nil {
259-
return ctrl.Result{}, err
263+
if statsusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Failed to reconcile ServiceMonitor"); statsusErr != nil {
264+
return ctrl.Result{}, statsusErr
260265
}
261266
return ctrl.Result{}, err
262267
} else if res.Requeue {
@@ -270,6 +275,19 @@ func (r *UnleashReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
270275
return ctrl.Result{}, err
271276
}
272277

278+
// Wait for Deployment rollout to finish before testing connection This is to
279+
// avoid testing connection to the previous instance if the Deployment is not
280+
// ready yet. Delay requeue to avoid tying up the reconciler since waiting is
281+
// done in the same reconcile loop.
282+
log.Info("Waiting for Deployment rollout to finish")
283+
err = r.waitForDeployment(ctx, deploymentTimeout, req.NamespacedName)
284+
if err != nil {
285+
if statusErr := r.updateStatusReconcileFailed(ctx, unleash, err, "Deployment rollout timed out"); statusErr != nil {
286+
return ctrl.Result{RequeueAfter: deploymentTimeout}, statusErr
287+
}
288+
return ctrl.Result{RequeueAfter: deploymentTimeout}, err
289+
}
290+
273291
// Set the reconcile status of the Unleash instance to available
274292
log.Info("Successfully reconciled Unleash resources")
275293
if err = r.updateStatusReconcileSuccess(ctx, unleash); err != nil {
@@ -279,28 +297,27 @@ func (r *UnleashReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
279297
// Test connection to Unleash instance
280298
stats, err := r.testConnection(unleash, ctx, log)
281299
if err != nil {
282-
if err := r.updateStatusConnectionFailed(ctx, unleash, err, "Failed to connect to Unleash instance"); err != nil {
283-
return ctrl.Result{}, err
300+
if statusErr := r.updateStatusConnectionFailed(ctx, unleash, err, "Failed to connect to Unleash instance"); statusErr != nil {
301+
return ctrl.Result{}, statusErr
284302
}
285303

286304
return ctrl.Result{}, err
287305
}
288306

289307
// Set the connection status of the Unleash instance to available
290308
log.Info("Successfully connected to Unleash instance", "version", stats.VersionOSS)
291-
err = r.updateStatusConnectionSuccess(ctx, unleash, stats)
292-
if err != nil {
293-
return ctrl.Result{}, err
309+
statusErr := r.updateStatusConnectionSuccess(ctx, unleash, stats)
310+
if statusErr != nil {
311+
return ctrl.Result{}, statusErr
294312
}
295313

296314
// Publish the Unleash instance to federation if enabled
297-
err = r.publish(ctx, unleash)
298-
if err != nil {
315+
if err = r.publish(ctx, unleash); err != nil {
299316
return ctrl.Result{}, err
300317
}
301318

302319
log.Info("Reconciliation of Unleash finished")
303-
return ctrl.Result{RequeueAfter: 1 * time.Hour}, nil
320+
return ctrl.Result{RequeueAfter: requeueAfter}, nil
304321
}
305322

306323
// publish the Unleash instance to pubsub if federation is enabled.
@@ -530,8 +547,6 @@ func (r *UnleashReconciler) reconcileIngress(ctx context.Context, unleash *unlea
530547
return ctrl.Result{}, nil
531548
}
532549

533-
// update prometheus metrics
534-
535550
return ctrl.Result{}, nil
536551
}
537552

@@ -677,6 +692,23 @@ func (r *UnleashReconciler) reconcileService(ctx context.Context, unleash *unlea
677692
return ctrl.Result{}, nil
678693
}
679694

695+
// waitForDeployment will wait for the deployment to be available
696+
func (r *UnleashReconciler) waitForDeployment(ctx context.Context, timeout time.Duration, key types.NamespacedName) error {
697+
ctx, cancel := context.WithTimeout(ctx, timeout)
698+
defer cancel()
699+
700+
err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) {
701+
deployment := &appsv1.Deployment{}
702+
if err := r.Client.Get(ctx, key, deployment); err != nil {
703+
return false, err
704+
}
705+
706+
return utils.DeploymentIsReady(deployment), nil
707+
})
708+
709+
return err
710+
}
711+
680712
// testConnection will test the connection to the Unleash instance
681713
func (r *UnleashReconciler) testConnection(unleash resources.UnleashInstance, ctx context.Context, log logr.Logger) (*unleashclient.InstanceAdminStatsResult, error) {
682714
client, err := unleash.ApiClient(ctx, r.Client, r.OperatorNamespace)

controllers/unleash_controller_test.go

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

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

119+
It("Should fail if Deployment rollout is not complete", func() {
120+
ctx := context.Background()
121+
122+
By("By creating a new Unleash")
123+
unleash := unleashResource("test-unleash-rollout-fail", UnleashNamespace, unleashv1.UnleashSpec{
124+
125+
Database: unleashv1.UnleashDatabaseConfig{
126+
URL: "postgres://unleash:unleash@unleash-postgres:5432/unleash?ssl=false",
127+
},
128+
})
129+
Expect(k8sClient.Create(ctx, unleash)).Should(Succeed())
130+
131+
By("By faking Deployment status as failed")
132+
createdDeployment := &appsv1.Deployment{}
133+
Eventually(getDeployment, timeout, interval).WithArguments(k8sClient, ctx, unleash.NamespacedName(), createdDeployment).Should(Succeed())
134+
setDeploymentStatusFailed(createdDeployment)
135+
Expect(k8sClient.Status().Update(ctx, createdDeployment)).Should(Succeed())
136+
137+
By("By checking that Unleash is failed")
138+
createdUnleash := &unleashv1.Unleash{ObjectMeta: unleash.ObjectMeta}
139+
Eventually(getUnleash, timeout, interval).WithArguments(k8sClient, ctx, createdUnleash).Should(ContainElement(metav1.Condition{
140+
Type: unleashv1.UnleashStatusConditionTypeReconciled,
141+
Status: metav1.ConditionFalse,
142+
Reason: "Reconciling",
143+
Message: "Deployment rollout timed out",
144+
}))
145+
Expect(createdUnleash.IsReady()).To(BeFalse())
146+
Expect(createdUnleash.Status.Reconciled).To(BeFalse())
147+
Expect(createdUnleash.Status.Connected).To(BeFalse())
148+
149+
By("By cleaning up the Unleash")
150+
Expect(k8sClient.Delete(ctx, createdUnleash)).Should(Succeed())
151+
})
152+
79153
PIt("Should fail when it cannot connect to Unleash")
80154

81155
It("Should succeed when it can connect to Unleash", func() {
@@ -89,6 +163,13 @@ var _ = Describe("Unleash controller", func() {
89163
})
90164
Expect(k8sClient.Create(ctx, unleash)).Should(Succeed())
91165

166+
By("By faking Deployment status as available")
167+
createdDeployment := &appsv1.Deployment{}
168+
Eventually(getDeployment, timeout, interval).WithArguments(k8sClient, ctx, unleash.NamespacedName(), createdDeployment).Should(Succeed())
169+
setDeploymentStatusAvailable(createdDeployment)
170+
Expect(k8sClient.Status().Update(ctx, createdDeployment)).Should(Succeed())
171+
172+
By("By checking that Unleash is connected")
92173
createdUnleash := &unleashv1.Unleash{ObjectMeta: unleash.ObjectMeta}
93174
Eventually(getUnleash, timeout, interval).WithArguments(k8sClient, ctx, createdUnleash).Should(ContainElement(metav1.Condition{
94175
Type: unleashv1.UnleashStatusConditionTypeConnected,
@@ -157,6 +238,13 @@ var _ = Describe("Unleash controller", func() {
157238
})
158239
Expect(k8sClient.Create(ctx, unleash)).Should(Succeed())
159240

241+
By("By faking Deployment status as available")
242+
createdDeployment := &appsv1.Deployment{}
243+
Eventually(getDeployment, timeout, interval).WithArguments(k8sClient, ctx, unleash.NamespacedName(), createdDeployment).Should(Succeed())
244+
setDeploymentStatusAvailable(createdDeployment)
245+
Expect(k8sClient.Status().Update(ctx, createdDeployment)).Should(Succeed())
246+
247+
By("By checking that Unleash is connected")
160248
createdUnleash := &unleashv1.Unleash{ObjectMeta: unleash.ObjectMeta}
161249
Eventually(getUnleash, timeout, interval).WithArguments(k8sClient, ctx, createdUnleash).Should(ContainElement(metav1.Condition{
162250
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.

0 commit comments

Comments
 (0)