package health import ( "context" "fmt" "os" "os/exec" "strings" "time" ) // NodeBlockLabel is the well-known label that workload authors set on the // local node to defer an OS update. When present and "true", apply refuses. const NodeBlockLabel = "updates.kubesolo.io/block" // CheckNodeBlocked returns (blocked, error). blocked==true means the local // node carries the updates.kubesolo.io/block=true label and the caller should // refuse the update. // // If the kubeconfig is not available (offline / pre-boot / air-gap), this // returns (false, nil) — silently allowing the update. That's the safe // behaviour for the air-gap case where the node may not be reachable from // the agent's perspective. func CheckNodeBlocked(kubeconfigPath string) (bool, error) { if kubeconfigPath == "" { kubeconfigPath = "/var/lib/kubesolo/pki/admin/admin.kubeconfig" } if _, err := os.Stat(kubeconfigPath); err != nil { // No kubeconfig — assume air-gap / pre-K8s. Don't block updates. return false, nil } // Query the node label via kubectl. We don't know the node name a // priori, so we use --kubeconfig on the local admin config and ask for // "the only node" (KubeSolo is single-node by design). ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "kubectl", "--kubeconfig", kubeconfigPath, "get", "node", "-o", `jsonpath={.items[0].metadata.labels.updates\.kubesolo\.io/block}`) out, err := cmd.Output() if err != nil { // API unreachable or no nodes — treat as not blocked (analogous to // the kubeconfig-missing case). We still surface the error so the // caller can decide to log it. return false, fmt.Errorf("query node label: %w", err) } return strings.TrimSpace(string(out)) == "true", nil }