Skip to content

Commit ce43a43

Browse files
authored
Merge pull request #18 from OpenSlides/feature/mschieder-bugfixes
Bugfixes for instance health progress bar
2 parents 247acf5 + e6a8330 commit ce43a43

7 files changed

Lines changed: 132 additions & 38 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,6 @@ Applied namespace: myinstancedirorg
348348
Applying stack manifests from: my.instance.dir.org/stack/
349349
...
350350
351-
Waiting for instance to become ready:
352351
[████████████████████████████████████████] Pods ready (13/13)
353352
354353
Instance is healthy: 13/13 pods ready

internal/constants/constants.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ const (
108108
BarStart string = "["
109109
BarEnd string = "]"
110110
ThrottleDuration time.Duration = 100 * time.Millisecond
111+
SpinnerType int = 14
112+
// add extra line at the end of progress bar detail buffer for pending pod names
113+
AddDetailLineBuffer int = 1
111114

112115
// wait function settings
113116
TickerDuration time.Duration = 2 * time.Second // checks health conditions every tick

internal/k8s/actions/apply.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ func applyManifest(ctx context.Context, k8sClient *client.Client, manifestPath s
3838
return "", fmt.Errorf("parsing YAML: %w", err)
3939
}
4040

41+
if obj.GetKind() == "" {
42+
logger.Info("Skipping manifest with no kind: %s", manifestPath)
43+
return "", nil
44+
}
45+
4146
namespace := obj.GetNamespace()
4247
if namespace == "" && obj.GetKind() == "Namespace" {
4348
namespace = obj.GetName()

internal/k8s/actions/health_check.go

Lines changed: 123 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package actions
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
"strings"
68
"time"
79

810
"github.com/OpenSlides/openslides-cli/internal/constants"
@@ -31,7 +33,15 @@ func getHealthStatus(ctx context.Context, k8sClient *client.Client, namespace st
3133
return nil, fmt.Errorf("listing pods: %w", err)
3234
}
3335

34-
total := len(pods.Items)
36+
var filteredPods []corev1.Pod
37+
for _, pod := range pods.Items {
38+
if pod.Status.Phase == corev1.PodSucceeded {
39+
continue
40+
}
41+
filteredPods = append(filteredPods, pod)
42+
}
43+
44+
total := len(filteredPods)
3545
if total == 0 {
3646
return &HealthStatus{
3747
Healthy: false,
@@ -42,17 +52,19 @@ func getHealthStatus(ctx context.Context, k8sClient *client.Client, namespace st
4252
}
4353

4454
ready := 0
45-
for _, pod := range pods.Items {
55+
for _, pod := range filteredPods {
4656
if isPodReady(&pod) {
4757
ready++
4858
}
4959
}
5060

61+
healthy := ready == total
62+
5163
return &HealthStatus{
52-
Healthy: ready == total,
64+
Healthy: healthy,
5365
Ready: ready,
5466
Total: total,
55-
Pods: pods.Items,
67+
Pods: filteredPods,
5668
}, nil
5769
}
5870

@@ -97,8 +109,6 @@ func checkHealth(ctx context.Context, k8sClient *client.Client, namespace string
97109

98110
// waitForInstanceHealthy waits for instance to become healthy
99111
func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, namespace string, timeout time.Duration) error {
100-
logger.Info("Waiting for instance to become healthy (timeout: %v)", timeout)
101-
102112
ticker := time.NewTicker(constants.TickerDuration)
103113
defer ticker.Stop()
104114

@@ -119,17 +129,27 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names
119129
lastStatus = status
120130

121131
if bar == nil && status.Total > 0 {
122-
bar = createProgressBar(status.Total, "Pods ready")
132+
bar = createProgressBar(status.Total, "Pods ready", constants.AddDetailLineBuffer)
133+
} else if bar != nil {
134+
bar.ChangeMax(status.Total)
123135
}
124-
125-
if bar != nil {
136+
if bar != nil && !bar.IsFinished() {
137+
notReady := getNotReadyNames(status.Pods)
138+
if len(notReady) > 0 {
139+
if err := bar.AddDetail(fmt.Sprintf("%s Pending: %s", constants.IconNotReady, strings.Join(notReady, ", "))); err != nil {
140+
return fmt.Errorf("adding pending pods detail: %w", err)
141+
}
142+
} else {
143+
if err := bar.AddDetail(""); err != nil {
144+
return fmt.Errorf("adding empty detail: %w", err)
145+
}
146+
}
126147
if err := bar.Set(status.Ready); err != nil {
127148
return fmt.Errorf("setting progress bar: %w", err)
128149
}
129150
}
130-
131151
if status.Healthy {
132-
if bar != nil {
152+
if bar != nil && !bar.IsFinished() {
133153
if err := bar.Finish(); err != nil {
134154
return fmt.Errorf("finishing progress bar: %w", err)
135155
}
@@ -139,7 +159,7 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names
139159
}
140160

141161
case <-timeoutCtx.Done():
142-
if bar != nil {
162+
if bar != nil && !bar.IsFinished() {
143163
if err := bar.Finish(); err != nil {
144164
return fmt.Errorf("finishing progress bar: %w", err)
145165
}
@@ -153,32 +173,64 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names
153173
}
154174
}
155175

156-
func createProgressBar(max int, description string) *progressbar.ProgressBar {
157-
return progressbar.NewOptions(max,
176+
func createProgressBar(max int, description string, maxDetailRow int) *progressbar.ProgressBar {
177+
opts := []progressbar.Option{
158178
progressbar.OptionSetDescription(description),
159179
progressbar.OptionSetWidth(constants.ProgressBarWidth),
160-
progressbar.OptionShowCount(),
180+
progressbar.OptionSetWriter(os.Stdout),
181+
progressbar.OptionSetMaxDetailRow(maxDetailRow),
161182
progressbar.OptionSetTheme(progressbar.Theme{
162183
Saucer: constants.Saucer,
163184
SaucerPadding: constants.SaucerPadding,
164185
BarStart: constants.BarStart,
165186
BarEnd: constants.BarEnd,
166187
}),
167188
progressbar.OptionThrottle(constants.ThrottleDuration),
168-
progressbar.OptionClearOnFinish(),
169-
)
189+
progressbar.OptionOnCompletion(func() {
190+
fmt.Println()
191+
}),
192+
}
193+
194+
if max > 0 {
195+
opts = append(opts, progressbar.OptionShowCount())
196+
} else {
197+
opts = append(opts, progressbar.OptionSpinnerType(constants.SpinnerType))
198+
}
199+
200+
return progressbar.NewOptions(max, opts...)
170201
}
171202

172203
// isPodReady checks if a pod is ready
173204
func isPodReady(pod *corev1.Pod) bool {
205+
if pod.DeletionTimestamp != nil {
206+
return false
207+
}
208+
174209
for _, condition := range pod.Status.Conditions {
175-
if condition.Type == corev1.PodReady {
176-
return condition.Status == corev1.ConditionTrue
210+
if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue {
211+
for _, container := range pod.Status.ContainerStatuses {
212+
if !container.Ready {
213+
return false
214+
}
215+
}
216+
return true
177217
}
178218
}
219+
179220
return false
180221
}
181222

223+
// getNotReadyNames
224+
func getNotReadyNames(pods []corev1.Pod) []string {
225+
var names []string
226+
for _, pod := range pods {
227+
if !isPodReady(&pod) {
228+
names = append(names, pod.Name)
229+
}
230+
}
231+
return names
232+
}
233+
182234
// namespaceIsActive checks if a namespace exists and is active
183235
func namespaceIsActive(ctx context.Context, k8sClient *client.Client, namespace string) (bool, error) {
184236
ns, err := k8sClient.Clientset().CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
@@ -229,6 +281,8 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names
229281
defer cancel()
230282

231283
var lastDeployment *appsv1.Deployment
284+
var bar *progressbar.ProgressBar
285+
232286
for {
233287
select {
234288
case <-ticker.C:
@@ -240,56 +294,92 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names
240294

241295
lastDeployment = deployment
242296

243-
if deployment.Status.ObservedGeneration >= deployment.Generation &&
244-
deployment.Status.UpdatedReplicas == *deployment.Spec.Replicas &&
245-
deployment.Status.AvailableReplicas == *deployment.Spec.Replicas &&
246-
deployment.Status.ReadyReplicas == *deployment.Spec.Replicas &&
247-
deployment.Status.Replicas == *deployment.Spec.Replicas {
297+
desired := int(*deployment.Spec.Replicas)
298+
updated := int(deployment.Status.UpdatedReplicas)
299+
ready := int(deployment.Status.ReadyReplicas)
300+
available := int(deployment.Status.AvailableReplicas)
301+
total := int(deployment.Status.Replicas)
302+
observedGen := deployment.Status.ObservedGeneration
303+
gen := deployment.Generation
248304

249-
logger.Info("Deployment %s is ready with %d replicas", deploymentName, *deployment.Spec.Replicas)
305+
if bar == nil && desired > 0 {
306+
bar = createProgressBar(-1, fmt.Sprintf("Waiting for %s deployment rollout", deploymentName), 0)
307+
}
308+
309+
if bar != nil {
310+
_ = bar.Add(1)
311+
}
312+
313+
if observedGen >= gen &&
314+
updated == desired &&
315+
available == desired &&
316+
ready == desired &&
317+
total == desired {
318+
if bar != nil {
319+
if err := bar.Finish(); err != nil {
320+
return fmt.Errorf("finishing progress bar: %w", err)
321+
}
322+
}
323+
logger.Info("Deployment %s is ready with %d replicas", deploymentName, desired)
250324
return nil
251325
}
252326

253-
logger.Debug("Deployment %s: %d/%d replicas ready, %d total (generation: %d/%d)",
327+
logger.Debug("Deployment %s: %d/%d updated, %d/%d ready, %d total (generation: %d/%d)",
254328
deploymentName,
255-
deployment.Status.ReadyReplicas,
256-
*deployment.Spec.Replicas,
257-
deployment.Status.Replicas,
258-
deployment.Status.ObservedGeneration,
259-
deployment.Generation)
329+
updated, desired,
330+
ready, desired,
331+
total,
332+
observedGen, gen)
260333

261334
case <-timeoutCtx.Done():
335+
if bar != nil {
336+
if err := bar.Finish(); err != nil {
337+
return fmt.Errorf("finishing progress bar: %w", err)
338+
}
339+
}
262340
logger.Warn("Timeout reached. Deployment status:")
263341
if lastDeployment != nil {
264342
printDeploymentStatus(namespace, deploymentName, lastDeployment)
265343
}
266344

267-
return fmt.Errorf("timeout waiting for deployment %s to become ready", deploymentName)
345+
return fmt.Errorf("timeout waiting for deployment %s rollout", deploymentName)
268346
}
269347
}
270348
}
271349

272350
// waitForNamespaceDeletion waits for a namespace to be completely deleted
273351
func waitForNamespaceDeletion(ctx context.Context, k8sClient *client.Client, namespace string, timeout time.Duration) error {
274352
clientset := k8sClient.Clientset()
275-
276353
ticker := time.NewTicker(constants.TickerDuration)
277354
defer ticker.Stop()
278355

279356
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
280357
defer cancel()
281358

359+
bar := createProgressBar(-1, fmt.Sprintf("Stopping %s", namespace), 0)
360+
282361
for {
283362
select {
284363
case <-ticker.C:
364+
_ = bar.Add(1)
285365
_, err := clientset.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
286366
if err != nil {
367+
if !errors.IsNotFound(err) {
368+
logger.Warn("Error checking namespace: %v", err)
369+
continue
370+
}
371+
if err := bar.Finish(); err != nil {
372+
return fmt.Errorf("finishing progress bar: %w", err)
373+
}
287374
logger.Debug("Namespace %s successfully deleted", namespace)
288375
return nil
289376
}
290377
logger.Debug("Namespace %s still terminating...", namespace)
291378

292379
case <-timeoutCtx.Done():
380+
if err := bar.Finish(); err != nil {
381+
return fmt.Errorf("finishing progress bar: %w", err)
382+
}
293383
return fmt.Errorf("timeout waiting for namespace %s to be deleted", namespace)
294384
}
295385
}

internal/k8s/actions/scale.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,12 @@ func ScaleCmd() *cobra.Command {
7474
return nil
7575
}
7676

77-
logger.Info("Waiting for deployment to become ready...")
7877
// Wait for the specific deployment (OpenSlides service name is deployment name)
7978
if err := waitForDeploymentReady(ctx, k8sClient, namespace, *service, *timeout); err != nil {
8079
return fmt.Errorf("waiting for deployment ready: %w", err)
8180
}
8281

83-
logger.Info("Service scaled successfully")
82+
logger.Info("%s service scaled successfully", *service)
8483
return nil
8584
}
8685

internal/k8s/actions/start.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ func StartCmd() *cobra.Command {
7676
return nil
7777
}
7878

79-
logger.Info("Waiting for instance to become ready...")
8079
if err := waitForInstanceHealthy(ctx, k8sClient, namespace, *timeout); err != nil {
8180
return fmt.Errorf("waiting for ready: %w", err)
8281
}

internal/k8s/actions/update_instance.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ func UpdateInstanceCmd() *cobra.Command {
7676
return nil
7777
}
7878

79-
logger.Info("Waiting for instance to become ready...")
8079
if err := waitForInstanceHealthy(ctx, k8sClient, namespace, *timeout); err != nil {
8180
return fmt.Errorf("waiting for instance health: %w", err)
8281
}

0 commit comments

Comments
 (0)