From 5d2f770055d5433d3727cc9521b1be89ced6a731 Mon Sep 17 00:00:00 2001 From: aantoni Date: Tue, 10 Feb 2026 17:42:48 +0100 Subject: [PATCH 1/5] working on progress bar changes --- internal/constants/constants.go | 1 + internal/k8s/actions/apply.go | 5 ++ internal/k8s/actions/health_check.go | 105 +++++++++++++++++++++------ internal/k8s/actions/scale.go | 3 +- 4 files changed, 91 insertions(+), 23 deletions(-) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index d06807b..d615983 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -108,6 +108,7 @@ const ( BarStart string = "[" BarEnd string = "]" ThrottleDuration time.Duration = 100 * time.Millisecond + SpinnerType int = 14 // wait function settings TickerDuration time.Duration = 2 * time.Second // checks health conditions every tick diff --git a/internal/k8s/actions/apply.go b/internal/k8s/actions/apply.go index f152b2b..c092289 100644 --- a/internal/k8s/actions/apply.go +++ b/internal/k8s/actions/apply.go @@ -38,6 +38,11 @@ func applyManifest(ctx context.Context, k8sClient *client.Client, manifestPath s return "", fmt.Errorf("parsing YAML: %w", err) } + if obj.GetKind() == "" { + logger.Debug("Skipping manifest with no kind: %s", manifestPath) + return "", nil + } + namespace := obj.GetNamespace() if namespace == "" && obj.GetKind() == "Namespace" { namespace = obj.GetName() diff --git a/internal/k8s/actions/health_check.go b/internal/k8s/actions/health_check.go index d693889..10a338a 100644 --- a/internal/k8s/actions/health_check.go +++ b/internal/k8s/actions/health_check.go @@ -31,7 +31,18 @@ func getHealthStatus(ctx context.Context, k8sClient *client.Client, namespace st return nil, fmt.Errorf("listing pods: %w", err) } - total := len(pods.Items) + var filteredPods []corev1.Pod + for _, pod := range pods.Items { + if pod.Status.Phase == corev1.PodSucceeded { + continue + } + if pod.DeletionTimestamp != nil { + continue + } + filteredPods = append(filteredPods, pod) + } + + total := len(filteredPods) if total == 0 { return &HealthStatus{ Healthy: false, @@ -42,7 +53,7 @@ func getHealthStatus(ctx context.Context, k8sClient *client.Client, namespace st } ready := 0 - for _, pod := range pods.Items { + for _, pod := range filteredPods { if isPodReady(&pod) { ready++ } @@ -52,7 +63,7 @@ func getHealthStatus(ctx context.Context, k8sClient *client.Client, namespace st Healthy: ready == total, Ready: ready, Total: total, - Pods: pods.Items, + Pods: filteredPods, }, nil } @@ -120,6 +131,8 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names if bar == nil && status.Total > 0 { bar = createProgressBar(status.Total, "Pods ready") + } else if bar != nil { + bar.ChangeMax(status.Total) } if bar != nil { @@ -133,6 +146,7 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } + fmt.Println() } logger.Info("Instance is healthy: %d/%d pods ready", status.Ready, status.Total) return nil @@ -143,6 +157,7 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } + fmt.Println() } logger.Warn("Timeout reached. Current status:") if lastStatus != nil { @@ -154,10 +169,9 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names } func createProgressBar(max int, description string) *progressbar.ProgressBar { - return progressbar.NewOptions(max, + opts := []progressbar.Option{ progressbar.OptionSetDescription(description), progressbar.OptionSetWidth(constants.ProgressBarWidth), - progressbar.OptionShowCount(), progressbar.OptionSetTheme(progressbar.Theme{ Saucer: constants.Saucer, SaucerPadding: constants.SaucerPadding, @@ -165,8 +179,15 @@ func createProgressBar(max int, description string) *progressbar.ProgressBar { BarEnd: constants.BarEnd, }), progressbar.OptionThrottle(constants.ThrottleDuration), - progressbar.OptionClearOnFinish(), - ) + } + + if max > 0 { + opts = append(opts, progressbar.OptionShowCount()) + } else { + opts = append(opts, progressbar.OptionSpinnerType(constants.SpinnerType)) + } + + return progressbar.NewOptions(max, opts...) } // isPodReady checks if a pod is ready @@ -229,6 +250,8 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names defer cancel() var lastDeployment *appsv1.Deployment + var bar *progressbar.ProgressBar + for { select { case <-ticker.C: @@ -240,31 +263,57 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names lastDeployment = deployment - if deployment.Status.ObservedGeneration >= deployment.Generation && - deployment.Status.UpdatedReplicas == *deployment.Spec.Replicas && - deployment.Status.AvailableReplicas == *deployment.Spec.Replicas && - deployment.Status.ReadyReplicas == *deployment.Spec.Replicas && - deployment.Status.Replicas == *deployment.Spec.Replicas { + desired := int(*deployment.Spec.Replicas) + updated := int(deployment.Status.UpdatedReplicas) + ready := int(deployment.Status.ReadyReplicas) + available := int(deployment.Status.AvailableReplicas) + total := int(deployment.Status.Replicas) + observedGen := deployment.Status.ObservedGeneration + gen := deployment.Generation + + if bar == nil && desired > 0 { + bar = createProgressBar(-1, fmt.Sprintf("Waiting for %s deployment rollout", deploymentName)) + } + + if bar != nil { + _ = bar.Add(1) + } - logger.Info("Deployment %s is ready with %d replicas", deploymentName, *deployment.Spec.Replicas) + if observedGen >= gen && + updated == desired && + available == desired && + ready == desired && + total == desired { + if bar != nil { + if err := bar.Finish(); err != nil { + return fmt.Errorf("finishing progress bar: %w", err) + } + fmt.Println() + } + logger.Info("Deployment %s is ready with %d replicas", deploymentName, desired) return nil } - logger.Debug("Deployment %s: %d/%d replicas ready, %d total (generation: %d/%d)", + logger.Debug("Deployment %s: %d/%d updated, %d/%d ready, %d total (generation: %d/%d)", deploymentName, - deployment.Status.ReadyReplicas, - *deployment.Spec.Replicas, - deployment.Status.Replicas, - deployment.Status.ObservedGeneration, - deployment.Generation) + updated, desired, + ready, desired, + total, + observedGen, gen) case <-timeoutCtx.Done(): + if bar != nil { + if err := bar.Finish(); err != nil { + return fmt.Errorf("finishing progress bar: %w", err) + } + fmt.Println() + } logger.Warn("Timeout reached. Deployment status:") if lastDeployment != nil { printDeploymentStatus(namespace, deploymentName, lastDeployment) } - return fmt.Errorf("timeout waiting for deployment %s to become ready", deploymentName) + return fmt.Errorf("timeout waiting for deployment %s rollout", deploymentName) } } } @@ -272,24 +321,38 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names // waitForNamespaceDeletion waits for a namespace to be completely deleted func waitForNamespaceDeletion(ctx context.Context, k8sClient *client.Client, namespace string, timeout time.Duration) error { clientset := k8sClient.Clientset() - ticker := time.NewTicker(constants.TickerDuration) defer ticker.Stop() timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + bar := createProgressBar(-1, fmt.Sprintf("Stopping %s", namespace)) + for { select { case <-ticker.C: + _ = bar.Add(1) _, err := clientset.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) if err != nil { + if !errors.IsNotFound(err) { + logger.Warn("Error checking namespace: %v", err) + continue + } + if err := bar.Finish(); err != nil { + return fmt.Errorf("finishing progress bar: %w", err) + } + fmt.Println() logger.Debug("Namespace %s successfully deleted", namespace) return nil } logger.Debug("Namespace %s still terminating...", namespace) case <-timeoutCtx.Done(): + if err := bar.Finish(); err != nil { + return fmt.Errorf("finishing progress bar: %w", err) + } + fmt.Println() return fmt.Errorf("timeout waiting for namespace %s to be deleted", namespace) } } diff --git a/internal/k8s/actions/scale.go b/internal/k8s/actions/scale.go index 788221d..a995b22 100644 --- a/internal/k8s/actions/scale.go +++ b/internal/k8s/actions/scale.go @@ -74,13 +74,12 @@ func ScaleCmd() *cobra.Command { return nil } - logger.Info("Waiting for deployment to become ready...") // Wait for the specific deployment (OpenSlides service name is deployment name) if err := waitForDeploymentReady(ctx, k8sClient, namespace, *service, *timeout); err != nil { return fmt.Errorf("waiting for deployment ready: %w", err) } - logger.Info("Service scaled successfully") + logger.Info("%s service scaled successfully", *service) return nil } From 8dbec639e471f5df3bfbf3bdebe61ef4947da8c3 Mon Sep 17 00:00:00 2001 From: aantoni Date: Wed, 11 Feb 2026 15:30:36 +0100 Subject: [PATCH 2/5] change loglevel for manifest file check to info --- internal/k8s/actions/apply.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/k8s/actions/apply.go b/internal/k8s/actions/apply.go index c092289..e277529 100644 --- a/internal/k8s/actions/apply.go +++ b/internal/k8s/actions/apply.go @@ -39,7 +39,7 @@ func applyManifest(ctx context.Context, k8sClient *client.Client, manifestPath s } if obj.GetKind() == "" { - logger.Debug("Skipping manifest with no kind: %s", manifestPath) + logger.Info("Skipping manifest with no kind: %s", manifestPath) return "", nil } From 2c812532d45c151b9eaa6f96529759686a4c8fda Mon Sep 17 00:00:00 2001 From: aantoni Date: Wed, 11 Feb 2026 15:36:55 +0100 Subject: [PATCH 3/5] add pod detail info to waitForInstanceHealthy progress bar --- internal/constants/constants.go | 2 ++ internal/k8s/actions/health_check.go | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index d615983..4d36a08 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -109,6 +109,8 @@ const ( BarEnd string = "]" ThrottleDuration time.Duration = 100 * time.Millisecond SpinnerType int = 14 + // add extra line at the end of progress bar detail buffer for newline via addDetail("") + AddDetailLineBuffer int = 1 // wait function settings TickerDuration time.Duration = 2 * time.Second // checks health conditions every tick diff --git a/internal/k8s/actions/health_check.go b/internal/k8s/actions/health_check.go index 10a338a..e9e0dc3 100644 --- a/internal/k8s/actions/health_check.go +++ b/internal/k8s/actions/health_check.go @@ -130,19 +130,30 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names lastStatus = status if bar == nil && status.Total > 0 { - bar = createProgressBar(status.Total, "Pods ready") + bar = createProgressBar(status.Total, "Pods ready", status.Total) } else if bar != nil { bar.ChangeMax(status.Total) } - - if bar != nil { + if bar != nil && !bar.IsFinished() { + for _, pod := range status.Pods { + icon := constants.IconReady + if !isPodReady(&pod) { + icon = constants.IconNotReady + } + if err := bar.AddDetail(fmt.Sprintf("%s %s", icon, pod.Name)); err != nil { + return fmt.Errorf("adding detail on pending pods: %w", err) + } + } + if err := bar.AddDetail(""); err != nil { + return fmt.Errorf("adding trailing newline detail: %w", err) + } if err := bar.Set(status.Ready); err != nil { return fmt.Errorf("setting progress bar: %w", err) } } if status.Healthy { - if bar != nil { + if bar != nil && !bar.IsFinished() { if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } @@ -153,7 +164,7 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names } case <-timeoutCtx.Done(): - if bar != nil { + if bar != nil && !bar.IsFinished() { if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } @@ -168,10 +179,11 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names } } -func createProgressBar(max int, description string) *progressbar.ProgressBar { +func createProgressBar(max int, description string, maxDetailRow int) *progressbar.ProgressBar { opts := []progressbar.Option{ progressbar.OptionSetDescription(description), progressbar.OptionSetWidth(constants.ProgressBarWidth), + progressbar.OptionSetMaxDetailRow(maxDetailRow + constants.AddDetailLineBuffer), progressbar.OptionSetTheme(progressbar.Theme{ Saucer: constants.Saucer, SaucerPadding: constants.SaucerPadding, @@ -272,7 +284,7 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names gen := deployment.Generation if bar == nil && desired > 0 { - bar = createProgressBar(-1, fmt.Sprintf("Waiting for %s deployment rollout", deploymentName)) + bar = createProgressBar(-1, fmt.Sprintf("Waiting for %s deployment rollout", deploymentName), 0) } if bar != nil { @@ -327,7 +339,7 @@ func waitForNamespaceDeletion(ctx context.Context, k8sClient *client.Client, nam timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - bar := createProgressBar(-1, fmt.Sprintf("Stopping %s", namespace)) + bar := createProgressBar(-1, fmt.Sprintf("Stopping %s", namespace), 0) for { select { From 1aea0e378b59e05fdfddcd3c8e6c3775f2e366f3 Mon Sep 17 00:00:00 2001 From: aantoni Date: Wed, 11 Feb 2026 15:59:41 +0100 Subject: [PATCH 4/5] remove redundant log line --- internal/k8s/actions/health_check.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/k8s/actions/health_check.go b/internal/k8s/actions/health_check.go index e9e0dc3..d6770ca 100644 --- a/internal/k8s/actions/health_check.go +++ b/internal/k8s/actions/health_check.go @@ -108,8 +108,6 @@ func checkHealth(ctx context.Context, k8sClient *client.Client, namespace string // waitForInstanceHealthy waits for instance to become healthy func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, namespace string, timeout time.Duration) error { - logger.Info("Waiting for instance to become healthy (timeout: %v)", timeout) - ticker := time.NewTicker(constants.TickerDuration) defer ticker.Stop() From e6a83306f0e78d124217ed6d1104c69d3110c23d Mon Sep 17 00:00:00 2001 From: aantoni Date: Thu, 12 Feb 2026 11:48:43 +0100 Subject: [PATCH 5/5] finish progress bar fixes, adds detail line for pending pods --- README.md | 1 - internal/constants/constants.go | 2 +- internal/k8s/actions/health_check.go | 65 ++++++++++++++++--------- internal/k8s/actions/start.go | 1 - internal/k8s/actions/update_instance.go | 1 - 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ffbe71e..7680394 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,6 @@ Applied namespace: myinstancedirorg Applying stack manifests from: my.instance.dir.org/stack/ ... -Waiting for instance to become ready: [████████████████████████████████████████] Pods ready (13/13) Instance is healthy: 13/13 pods ready diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 4d36a08..82de9f4 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -109,7 +109,7 @@ const ( BarEnd string = "]" ThrottleDuration time.Duration = 100 * time.Millisecond SpinnerType int = 14 - // add extra line at the end of progress bar detail buffer for newline via addDetail("") + // add extra line at the end of progress bar detail buffer for pending pod names AddDetailLineBuffer int = 1 // wait function settings diff --git a/internal/k8s/actions/health_check.go b/internal/k8s/actions/health_check.go index d6770ca..74e57b1 100644 --- a/internal/k8s/actions/health_check.go +++ b/internal/k8s/actions/health_check.go @@ -3,6 +3,8 @@ package actions import ( "context" "fmt" + "os" + "strings" "time" "github.com/OpenSlides/openslides-cli/internal/constants" @@ -36,9 +38,6 @@ func getHealthStatus(ctx context.Context, k8sClient *client.Client, namespace st if pod.Status.Phase == corev1.PodSucceeded { continue } - if pod.DeletionTimestamp != nil { - continue - } filteredPods = append(filteredPods, pod) } @@ -59,8 +58,10 @@ func getHealthStatus(ctx context.Context, k8sClient *client.Client, namespace st } } + healthy := ready == total + return &HealthStatus{ - Healthy: ready == total, + Healthy: healthy, Ready: ready, Total: total, Pods: filteredPods, @@ -128,34 +129,30 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names lastStatus = status if bar == nil && status.Total > 0 { - bar = createProgressBar(status.Total, "Pods ready", status.Total) + bar = createProgressBar(status.Total, "Pods ready", constants.AddDetailLineBuffer) } else if bar != nil { bar.ChangeMax(status.Total) } if bar != nil && !bar.IsFinished() { - for _, pod := range status.Pods { - icon := constants.IconReady - if !isPodReady(&pod) { - icon = constants.IconNotReady + notReady := getNotReadyNames(status.Pods) + if len(notReady) > 0 { + if err := bar.AddDetail(fmt.Sprintf("%s Pending: %s", constants.IconNotReady, strings.Join(notReady, ", "))); err != nil { + return fmt.Errorf("adding pending pods detail: %w", err) } - if err := bar.AddDetail(fmt.Sprintf("%s %s", icon, pod.Name)); err != nil { - return fmt.Errorf("adding detail on pending pods: %w", err) + } else { + if err := bar.AddDetail(""); err != nil { + return fmt.Errorf("adding empty detail: %w", err) } } - if err := bar.AddDetail(""); err != nil { - return fmt.Errorf("adding trailing newline detail: %w", err) - } if err := bar.Set(status.Ready); err != nil { return fmt.Errorf("setting progress bar: %w", err) } } - if status.Healthy { if bar != nil && !bar.IsFinished() { if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } - fmt.Println() } logger.Info("Instance is healthy: %d/%d pods ready", status.Ready, status.Total) return nil @@ -166,7 +163,6 @@ func waitForInstanceHealthy(ctx context.Context, k8sClient *client.Client, names if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } - fmt.Println() } logger.Warn("Timeout reached. Current status:") if lastStatus != nil { @@ -181,7 +177,8 @@ func createProgressBar(max int, description string, maxDetailRow int) *progressb opts := []progressbar.Option{ progressbar.OptionSetDescription(description), progressbar.OptionSetWidth(constants.ProgressBarWidth), - progressbar.OptionSetMaxDetailRow(maxDetailRow + constants.AddDetailLineBuffer), + progressbar.OptionSetWriter(os.Stdout), + progressbar.OptionSetMaxDetailRow(maxDetailRow), progressbar.OptionSetTheme(progressbar.Theme{ Saucer: constants.Saucer, SaucerPadding: constants.SaucerPadding, @@ -189,6 +186,9 @@ func createProgressBar(max int, description string, maxDetailRow int) *progressb BarEnd: constants.BarEnd, }), progressbar.OptionThrottle(constants.ThrottleDuration), + progressbar.OptionOnCompletion(func() { + fmt.Println() + }), } if max > 0 { @@ -202,14 +202,35 @@ func createProgressBar(max int, description string, maxDetailRow int) *progressb // isPodReady checks if a pod is ready func isPodReady(pod *corev1.Pod) bool { + if pod.DeletionTimestamp != nil { + return false + } + for _, condition := range pod.Status.Conditions { - if condition.Type == corev1.PodReady { - return condition.Status == corev1.ConditionTrue + if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue { + for _, container := range pod.Status.ContainerStatuses { + if !container.Ready { + return false + } + } + return true } } + return false } +// getNotReadyNames +func getNotReadyNames(pods []corev1.Pod) []string { + var names []string + for _, pod := range pods { + if !isPodReady(&pod) { + names = append(names, pod.Name) + } + } + return names +} + // namespaceIsActive checks if a namespace exists and is active func namespaceIsActive(ctx context.Context, k8sClient *client.Client, namespace string) (bool, error) { ns, err := k8sClient.Clientset().CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) @@ -298,7 +319,6 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } - fmt.Println() } logger.Info("Deployment %s is ready with %d replicas", deploymentName, desired) return nil @@ -316,7 +336,6 @@ func waitForDeploymentReady(ctx context.Context, k8sClient *client.Client, names if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } - fmt.Println() } logger.Warn("Timeout reached. Deployment status:") if lastDeployment != nil { @@ -352,7 +371,6 @@ func waitForNamespaceDeletion(ctx context.Context, k8sClient *client.Client, nam if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } - fmt.Println() logger.Debug("Namespace %s successfully deleted", namespace) return nil } @@ -362,7 +380,6 @@ func waitForNamespaceDeletion(ctx context.Context, k8sClient *client.Client, nam if err := bar.Finish(); err != nil { return fmt.Errorf("finishing progress bar: %w", err) } - fmt.Println() return fmt.Errorf("timeout waiting for namespace %s to be deleted", namespace) } } diff --git a/internal/k8s/actions/start.go b/internal/k8s/actions/start.go index b18a749..0d1b643 100644 --- a/internal/k8s/actions/start.go +++ b/internal/k8s/actions/start.go @@ -76,7 +76,6 @@ func StartCmd() *cobra.Command { return nil } - logger.Info("Waiting for instance to become ready...") if err := waitForInstanceHealthy(ctx, k8sClient, namespace, *timeout); err != nil { return fmt.Errorf("waiting for ready: %w", err) } diff --git a/internal/k8s/actions/update_instance.go b/internal/k8s/actions/update_instance.go index 3d6b66b..1ebed7e 100644 --- a/internal/k8s/actions/update_instance.go +++ b/internal/k8s/actions/update_instance.go @@ -76,7 +76,6 @@ func UpdateInstanceCmd() *cobra.Command { return nil } - logger.Info("Waiting for instance to become ready...") if err := waitForInstanceHealthy(ctx, k8sClient, namespace, *timeout); err != nil { return fmt.Errorf("waiting for instance health: %w", err) }