Ctrl K
ComunidadGuías…

pkg/health/workload_rollout.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package health

import (
	"fmt"
	"strconv"
	"strings"

	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

type RolloutPhase string

const (
	RolloutIdle             RolloutPhase = "idle"
	RolloutApplying         RolloutPhase = "applying"
	RolloutProgressing      RolloutPhase = "progressing"
	RolloutWaiting          RolloutPhase = "waiting"
	RolloutPaused           RolloutPhase = "paused"
	RolloutPartitionReached RolloutPhase = "partition-reached"
	RolloutStalled          RolloutPhase = "stalled"
)

type WorkloadRolloutActivity struct {
	Phase     RolloutPhase `json:"phase"`
	Active    bool         `json:"active"`
	Manual    bool         `json:"manual"`
	Label     string       `json:"label"`
	Detail    string       `json:"detail,omitempty"`
	Desired   int32        `json:"desired"`
	Updated   int32        `json:"updated"`
	Ready     int32        `json:"ready"`
	Available int32        `json:"available"`
}

func WorkloadRollout(obj any) WorkloadRolloutActivity {
	switch o := obj.(type) {
	case *appsv1.Deployment:
		return deploymentRollout(o)
	case *appsv1.StatefulSet:
		return statefulSetRollout(o)
	case *appsv1.DaemonSet:
		return daemonSetRollout(o)
	case *unstructured.Unstructured:
		if strings.EqualFold(o.GetKind(), "Rollout") && strings.HasPrefix(o.GetAPIVersion(), "argoproj.io/") {
			return argoRollout(o)
		}
	}
	return activity(RolloutIdle, false, "Stable", "", 0, 0, 0, 0)
}

func deploymentRollout(d *appsv1.Deployment) WorkloadRolloutActivity {
	desired := specReplicas(d.Spec.Replicas)
	updated := d.Status.UpdatedReplicas
	ready := d.Status.ReadyReplicas
	available := d.Status.AvailableReplicas
	base := activityCounts(desired, updated, ready, available)

	if d.Status.ObservedGeneration < d.Generation {
		return base.with(RolloutApplying, true, "Applying change", "Waiting for the Deployment controller to observe generation")
	}
	if conditionFailed(d.Status.Conditions, "Progressing", "ProgressDeadlineExceeded") {
		return base.with(RolloutStalled, false, "Rollout stalled", conditionMessage(d.Status.Conditions, "Progressing", "Progress deadline exceeded"))
	}
	old := d.Status.Replicas - updated
	if old < 0 {
		old = 0
	}
	pending := updated < desired || old > 0
	if d.Spec.Paused {
		detail := "No rollout is pending"
		if pending {
			detail = fmt.Sprintf("Paused at %d/%d updated", updated, desired)
		}
		return base.with(RolloutPaused, pending, "Rollout paused", detail)
	}
	if desired == 0 {
		return base.with(RolloutIdle, false, "Scaled to zero", "No replicas requested")
	}
	if !pending {
		return base.with(RolloutIdle, false, "Stable", fmt.Sprintf("%d/%d available", available, desired))
	}
	if d.Spec.Strategy.Type == appsv1.RecreateDeploymentStrategyType {
		if updated == 0 && old > 0 {
			return base.with(RolloutWaiting, true, "Replacing replicas", fmt.Sprintf("Waiting for old replicas to stop · %d available", available))
		}
		if updated == 0 {
			return base.with(RolloutWaiting, true, "Waiting for new revision", replicaDetail(updated, desired, available))
		}
		return base.with(RolloutProgressing, true, "Replacing replicas", replicaDetail(updated, desired, available))
	}
	if updated == 0 {
		return base.with(RolloutWaiting, true, "Waiting for new revision", replicaDetail(updated, desired, available))
	}
	if old == 0 && updated == d.Status.Replicas {
		return base.with(RolloutProgressing, true, "Scaling", replicaDetail(updated, desired, available))
	}
	return base.with(RolloutProgressing, true, "Rolling out", replicaDetail(updated, desired, available))
}

func statefulSetRollout(s *appsv1.StatefulSet) WorkloadRolloutActivity {
	desired := specReplicas(s.Spec.Replicas)
	updated := s.Status.UpdatedReplicas
	ready := s.Status.ReadyReplicas
	base := activityCounts(desired, updated, ready, ready)

	if s.Status.ObservedGeneration < s.Generation {
		return base.with(RolloutApplying, true, "Applying change", "Waiting for the StatefulSet controller to observe generation")
	}
	if desired == 0 {
		return base.with(RolloutIdle, false, "Scaled to zero", "No replicas requested")
	}
	if s.Spec.UpdateStrategy.Type == appsv1.OnDeleteStatefulSetStrategyType {
		if updated == desired && ready == desired {
			return base.with(RolloutIdle, false, "Stable", fmt.Sprintf("%d/%d ready", ready, desired))
		}
		return base.withAction(RolloutWaiting, true, "Waiting for Pod restart", fmt.Sprintf("OnDelete strategy · %d/%d updated", updated, desired))
	}
	partition := int32(0)
	if s.Spec.UpdateStrategy.RollingUpdate != nil && s.Spec.UpdateStrategy.RollingUpdate.Partition != nil {
		partition = *s.Spec.UpdateStrategy.RollingUpdate.Partition
	}
	target := desired - partition
	if target < 0 {
		target = 0
	}
	if updated >= target {
		if partition > 0 && s.Status.CurrentRevision != "" && s.Status.UpdateRevision != "" && s.Status.CurrentRevision != s.Status.UpdateRevision {
			return base.with(RolloutPartitionReached, false, "Partition reached", fmt.Sprintf("%d/%d Pods intentionally retained", partition, desired))
		}
		return base.with(RolloutIdle, false, "Stable", fmt.Sprintf("%d/%d ready", ready, desired))
	}
	if updated == 0 {
		return base.with(RolloutWaiting, true, "Waiting for new revision", fmt.Sprintf("%d/%d target Pods updated · %d/%d ready", updated, target, ready, desired))
	}
	return base.with(RolloutProgressing, true, "Rolling out", fmt.Sprintf("%d/%d target Pods updated · %d/%d ready", updated, target, ready, desired))
}

func daemonSetRollout(d *appsv1.DaemonSet) WorkloadRolloutActivity {
	desired := d.Status.DesiredNumberScheduled
	updated := d.Status.UpdatedNumberScheduled
	ready := d.Status.NumberReady
	available := d.Status.NumberAvailable
	base := activityCounts(desired, updated, ready, available)

	if d.Status.ObservedGeneration < d.Generation {
		return base.with(RolloutApplying, true, "Applying change", "Waiting for the DaemonSet controller to observe generation")
	}
	if desired == 0 {
		return base.with(RolloutIdle, false, "No targets", "Selector matches no nodes")
	}
	if d.Spec.UpdateStrategy.Type == appsv1.OnDeleteDaemonSetStrategyType {
		if updated == desired && available == desired {
			return base.with(RolloutIdle, false, "Stable", fmt.Sprintf("%d/%d available", available, desired))
		}
		return base.withAction(RolloutWaiting, true, "Waiting for Pod restart", fmt.Sprintf("OnDelete strategy · %d/%d updated", updated, desired))
	}
	if updated == desired {
		return base.with(RolloutIdle, false, "Stable", fmt.Sprintf("%d/%d available", available, desired))
	}
	if updated == 0 {
		return base.with(RolloutWaiting, true, "Waiting for new revision", replicaDetail(updated, desired, available))
	}
	return base.with(RolloutProgressing, true, "Rolling out", replicaDetail(updated, desired, available))
}

func argoRollout(r *unstructured.Unstructured) WorkloadRolloutActivity {
	desired := nestedInt32(r.Object, "spec", "replicas")
	if _, found, _ := unstructured.NestedFieldNoCopy(r.Object, "spec", "replicas"); !found {
		desired = nestedInt32(r.Object, "status", "replicas")
		if desired == 0 {
			desired = 1
		}
	}
	updated := nestedInt32(r.Object, "status", "updatedReplicas")
	ready := nestedInt32(r.Object, "status", "readyReplicas")
	available := nestedInt32(r.Object, "status", "availableReplicas")
	base := activityCounts(desired, updated, ready, available)
	phase, _, _ := unstructured.NestedString(r.Object, "status", "phase")
	message, _, _ := unstructured.NestedString(r.Object, "status", "message")
	aborted, _, _ := unstructured.NestedBool(r.Object, "status", "abort")
	if observed, found, _ := unstructured.NestedString(r.Object, "status", "observedGeneration"); found {
		generation, err := strconv.ParseInt(observed, 10, 64)
		if err == nil && generation > 0 && generation < r.GetGeneration() {
			return base.with(RolloutApplying, true, "Applying change", "Waiting for the Rollout controller to observe generation")
		}
	}
	if failedMessage, failed := argoFailureMessage(r); failed {
		if failedMessage == "" {
			failedMessage = message
		}
		if failedMessage == "" {
			failedMessage = "The Rollout controller reported a failed revision"
		}
		return base.with(RolloutStalled, false, "Rollout stalled", failedMessage)
	}
	if aborted {
		if message == "" {
			message = "The Rollout was aborted"
		}
		return base.with(RolloutStalled, false, "Rollout stalled", message)
	}
	switch strings.ToLower(phase) {
	case "degraded", "error", "failed", "aborted":
		if message == "" {
			message = "The Rollout controller reported a failed revision"
		}
		return base.with(RolloutStalled, false, "Rollout stalled", message)
	case "paused":
		return base.with(RolloutPaused, true, "Rollout paused", argoStepDetail(r, updated, desired, available))
	case "progressing":
		if updated == 0 {
			return base.with(RolloutWaiting, true, "Waiting for new revision", argoStepDetail(r, updated, desired, available))
		}
		return base.with(RolloutProgressing, true, "Rolling out", argoStepDetail(r, updated, desired, available))
	case "healthy":
		return base.with(RolloutIdle, false, "Stable", fmt.Sprintf("%d/%d available", available, desired))
	default:
		if desired > 0 && (updated < desired || available < desired) {
			return base.with(RolloutApplying, true, "Applying change", argoStepDetail(r, updated, desired, available))
		}
		return base.with(RolloutIdle, false, "Stable", message)
	}
}

func argoFailureMessage(r *unstructured.Unstructured) (string, bool) {
	conditions, _, _ := unstructured.NestedSlice(r.Object, "status", "conditions")
	for _, raw := range conditions {
		condition, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		conditionType, _ := condition["type"].(string)
		conditionStatus, _ := condition["status"].(string)
		reason, _ := condition["reason"].(string)
		failed := (conditionType == "InvalidSpec" && conditionStatus == "True") ||
			(conditionType == "Progressing" && (conditionStatus == "False" || reason == "ProgressDeadlineExceeded"))
		if !failed {
			continue
		}
		if message, _ := condition["message"].(string); message != "" {
			return message, true
		}
		return reason, true
	}
	return "", false
}

func argoStepDetail(r *unstructured.Unstructured, updated, desired, available int32) string {
	// A mesh-routed Rollout can report status.canary.weights with no
	// explicit canary steps at all (the mesh integration drives the split,
	// not a step list) — the weight suffix applies independent of whether a
	// step prefix does.
	weightSuffix := ""
	if weight, wFound, _ := unstructured.NestedInt64(r.Object, "status", "canary", "weights", "canary", "weight"); wFound {
		weightSuffix = fmt.Sprintf(" · %d%% canary traffic", weight)
	}

	steps, stepsFound, _ := unstructured.NestedSlice(r.Object, "spec", "strategy", "canary", "steps")
	step, found, _ := unstructured.NestedInt64(r.Object, "status", "currentStepIndex")
	if !found || !stepsFound || len(steps) == 0 {
		return replicaDetail(updated, desired, available) + weightSuffix
	}
	displayStep := step + 1
	if displayStep < 1 {
		displayStep = 1
	}
	if displayStep > int64(len(steps)) {
		displayStep = int64(len(steps))
	}
	label := ""
	if currentStep, ok := steps[displayStep-1].(map[string]any); ok {
		label = fmt.Sprintf(" (%s)", canaryStepLabel(currentStep))
	}
	return fmt.Sprintf("Step %d%s · %d/%d updated · %d available%s", displayStep, label, updated, desired, available, weightSuffix)
}

// canaryStepLabel mirrors packages/k8s-ui/src/components/resources/renderers/
// RolloutRenderer.tsx's canaryStepLabel exactly (same output strings, same
// step-type coverage) - the two are checked against the same golden fixture
// (testdata/workload_rollout_vectors.json), so a change to one without the
// other silently breaks cross-language parity rather than failing loudly.
func canaryStepLabel(step map[string]any) string {
	if len(step) == 0 {
		return "Unknown step"
	}

	if weight, ok := step["setWeight"]; ok {
		return fmt.Sprintf("Set weight: %s%%", stepNumberString(weight))
	}

	if pause, ok := step["pause"].(map[string]any); ok {
		if duration, ok := pause["duration"]; ok && duration != nil && duration != "" {
			return fmt.Sprintf("Pause: %v", duration)
		}
		return "Pause: until promoted"
	}
	if _, ok := step["pause"]; ok {
		return "Pause: until promoted"
	}

	if analysis, ok := step["analysis"].(map[string]any); ok {
		names := templateNames(analysis["templates"])
		if len(names) > 0 {
			return fmt.Sprintf("Analysis: %s", strings.Join(names, ", "))
		}
		return "Analysis"
	}

	if experiment, ok := step["experiment"].(map[string]any); ok {
		names := experimentTemplateNames(experiment["templates"])
		duration := ""
		if d, ok := experiment["duration"]; ok && d != nil && d != "" {
			duration = fmt.Sprintf(" for %v", d)
		}
		if len(names) > 0 {
			return fmt.Sprintf("Experiment: %s%s", strings.Join(names, ", "), duration)
		}
		return fmt.Sprintf("Experiment%s", duration)
	}

	if scale, ok := step["setCanaryScale"].(map[string]any); ok {
		if match, _ := scale["matchTrafficWeight"].(bool); match {
			return "Set canary scale: match traffic weight"
		}
		if replicas, ok := scale["replicas"]; ok {
			return fmt.Sprintf("Set canary scale: %s replicas", stepNumberString(replicas))
		}
		if weight, ok := scale["weight"]; ok {
			return fmt.Sprintf("Set canary scale: %s%%", stepNumberString(weight))
		}
		return "Set canary scale"
	}

	if route, ok := step["setHeaderRoute"].(map[string]any); ok {
		name, _ := route["name"].(string)
		match, _ := route["match"].([]any)
		if len(match) == 0 {
			if name != "" {
				return fmt.Sprintf("Remove header route: %s", name)
			}
			return "Remove header route"
		}
		var headers []string
		for _, m := range match {
			if mm, ok := m.(map[string]any); ok {
				if headerName, ok := mm["headerName"].(string); ok && headerName != "" {
					headers = append(headers, headerName)
				}
			}
		}
		label := "Header route"
		if name != "" {
			label += " " + name
		}
		if len(headers) > 0 {
			label += ": " + strings.Join(headers, ", ")
		}
		return label
	}

	if route, ok := step["setMirrorRoute"].(map[string]any); ok {
		name, _ := route["name"].(string)
		match, _ := route["match"].([]any)
		if len(match) == 0 {
			if name != "" {
				return fmt.Sprintf("Remove mirror route: %s", name)
			}
			return "Remove mirror route"
		}
		label := "Mirror route"
		if name != "" {
			label += " " + name
		}
		if pct, ok := route["percentage"]; ok {
			label += fmt.Sprintf(" (%s%%)", stepNumberString(pct))
		}
		return label
	}

	if plugin, ok := step["plugin"].(map[string]any); ok {
		name, _ := plugin["name"].(string)
		if name == "" {
			name = "unnamed"
		}
		return fmt.Sprintf("Plugin: %s", name)
	}

	for key := range step {
		return fmt.Sprintf("Unrecognized step: %s", key)
	}
	return "Unknown step"
}

// templateNames extracts templateName/clusterTemplateName from an
// analysis.templates list (AnalysisTemplate/ClusterAnalysisTemplate refs),
// same as the TS version. NOT used for experiment.templates — those are pod
// templates, a different shape entirely; see experimentTemplateNames.
func templateNames(raw any) []string {
	list, ok := raw.([]any)
	if !ok {
		return nil
	}
	var names []string
	for _, item := range list {
		t, ok := item.(map[string]any)
		if !ok {
			continue
		}
		if name, ok := t["templateName"].(string); ok && name != "" {
			names = append(names, name)
			continue
		}
		if name, ok := t["clusterTemplateName"].(string); ok && name != "" {
			names = append(names, name)
		}
	}
	return names
}

// experimentTemplateNames extracts `name` from an experiment.templates
// list — Argo's RolloutExperimentTemplate entries (pod template variants
// like "baseline"/"canary" an Experiment spins up replicas of) carry their
// identifier under `name`, not `templateName`/`clusterTemplateName` (the
// AnalysisTemplate ref shape templateNames handles). Mirrors the TS version.
func experimentTemplateNames(raw any) []string {
	list, ok := raw.([]any)
	if !ok {
		return nil
	}
	var names []string
	for _, item := range list {
		t, ok := item.(map[string]any)
		if !ok {
			continue
		}
		if name, ok := t["name"].(string); ok && name != "" {
			names = append(names, name)
		}
	}
	return names
}

// stepNumberString formats a JSON-decoded numeric value the way it appears
// in the Rollout spec (e.g. int64(20) -> "20"), matching JS's implicit
// number->string coercion used by the TS version's template literals.
func stepNumberString(v any) string {
	switch n := v.(type) {
	case int64:
		return strconv.FormatInt(n, 10)
	case float64:
		return strconv.FormatFloat(n, 'f', -1, 64)
	default:
		return fmt.Sprintf("%v", v)
	}
}

func conditionFailed(conditions []appsv1.DeploymentCondition, conditionType appsv1.DeploymentConditionType, reason string) bool {
	for _, condition := range conditions {
		if condition.Type == conditionType && (condition.Status == "False" || condition.Reason == reason) {
			return true
		}
	}
	return false
}

func conditionMessage(conditions []appsv1.DeploymentCondition, conditionType appsv1.DeploymentConditionType, fallback string) string {
	for _, condition := range conditions {
		if condition.Type == conditionType && condition.Message != "" {
			return condition.Message
		}
	}
	return fallback
}

func nestedInt32(obj map[string]any, fields ...string) int32 {
	value, found, _ := unstructured.NestedInt64(obj, fields...)
	if found {
		return int32(value)
	}
	return 0
}

func activityCounts(desired, updated, ready, available int32) WorkloadRolloutActivity {
	return activity(RolloutIdle, false, "Stable", "", desired, updated, ready, available)
}

func activity(phase RolloutPhase, active bool, label, detail string, desired, updated, ready, available int32) WorkloadRolloutActivity {
	return WorkloadRolloutActivity{Phase: phase, Active: active, Label: label, Detail: detail, Desired: desired, Updated: updated, Ready: ready, Available: available}
}

func (a WorkloadRolloutActivity) with(phase RolloutPhase, active bool, label, detail string) WorkloadRolloutActivity {
	a.Phase = phase
	a.Active = active
	a.Manual = false
	a.Label = label
	a.Detail = detail
	return a
}

func (a WorkloadRolloutActivity) withAction(phase RolloutPhase, active bool, label, detail string) WorkloadRolloutActivity {
	a = a.with(phase, active, label, detail)
	a.Manual = true
	return a
}

func replicaDetail(updated, desired, available int32) string {
	return fmt.Sprintf("%d/%d updated · %d available", updated, desired, available)
}