Files
zeekayandHanzo Dev 9264049f1d A health address is built in one place, like a chain address
`lux dev start` could not bring up a node built from the current tree, for
three reasons that share a shape with the chain-segment rename: the CLI
spelled out an address the node had already left behind, and nothing failed
to compile.

The health service moved onto typed ops. /v1/health is gone, so every probe
written against it read a healthy node as unhealthy and every printed
endpoint sent a reader to a 404. route.Health, route.Liveness and
route.Readiness compose the three addresses the node answers on, next to
route.Chain and off the same base, and the ten call sites that had it
written out now ask for the one they mean: a probe polls readiness, a
report reads the full checks, a printed endpoint offers the report.

Two luxd flags no longer exist, and passing an unknown flag is fatal, so
the node refused the config before it read any of it:

  --api-keystore-enabled  the keystore API is gone
  --automine-interval     block cadence is the C-Chain's own
                          enable-automining, read by the block builder from
                          the chain config dir, not a node flag

The --automine duration flag went with the second: it named a knob that
does not exist.

`lux dev stop` looked only in the default data-dir, and otherwise pkill'd
anything whose command line carried a quorum size of one — a pattern that
fits every dev node on the machine, not just the one this command started.
It takes the same --data-dir as start now, resolved by the same function,
and reads the PID from there; no PID file is an answer rather than a reason
to guess.

Health gets composition but not the guard that pins chain addresses:
pkg/ssh still asks for health as a JSON-RPC method, and the whole
JSON-RPC-over-SSH family it belongs to is owed the same move onto typed
ops. A guard added before that migration would need an exemption for it,
and an exemption is how a guard rots.

Measured against luxd 1.36.181 (constants v1.6.4): start reaches ready,
prints /v1/chain/C/rpc and /v1/health/ops/health, both answer 200, and
stop interrupts the node it started and reports plainly when there is
none.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-05 19:06:43 -07:00

125 lines
3.2 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package nodecmd
import (
"context"
"fmt"
"os"
"path/filepath"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
// k8s flags shared across subcommands
var (
flagContext string // --context (k8s context override)
flagNamespace string // --namespace (override auto-detection)
flagMainnet bool // --mainnet
flagTestnet bool // --testnet
flagDevnet bool // --devnet
)
const (
statefulSetName = "luxd"
containerName = "luxd"
defaultHTTPPort = int32(9630)
)
// resolveNamespace returns the k8s namespace from flags.
func resolveNamespace() (string, error) {
if flagNamespace != "" {
return flagNamespace, nil
}
set := 0
if flagMainnet {
set++
}
if flagTestnet {
set++
}
if flagDevnet {
set++
}
if set > 1 {
return "", fmt.Errorf("specify exactly one of --mainnet, --testnet, or --devnet")
}
switch {
case flagMainnet:
return "lux-mainnet", nil
case flagTestnet:
return "lux-testnet", nil
case flagDevnet:
return "lux-devnet", nil
default:
return "", fmt.Errorf("specify --mainnet, --testnet, --devnet, or --namespace")
}
}
// newK8sClient creates a kubernetes clientset using kubeconfig with optional context override.
func newK8sClient() (*kubernetes.Clientset, error) {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("cannot determine home directory: %w", err)
}
kubeconfig = filepath.Join(home, ".kube", "config")
}
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = kubeconfig
overrides := &clientcmd.ConfigOverrides{}
if flagContext != "" {
overrides.CurrentContext = flagContext
}
cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides).ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
}
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create k8s client: %w", err)
}
return client, nil
}
// podReady checks if a pod's readiness conditions indicate it is ready.
func podReady(ctx context.Context, client *kubernetes.Clientset, namespace, podName string) (bool, error) {
pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return false, err
}
for _, cond := range pod.Status.Conditions {
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
return true, nil
}
}
return false, nil
}
// podImage returns the current image of the luxd container in a pod.
func podImage(ctx context.Context, client *kubernetes.Clientset, namespace, podName string) (string, error) {
pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return "", err
}
for _, c := range pod.Status.ContainerStatuses {
if c.Name == containerName {
return c.Image, nil
}
}
return "", fmt.Errorf("container %s not found in pod %s", containerName, podName)
}
// int32Ptr returns a pointer to an int32 value.
func int32Ptr(i int32) *int32 {
return &i
}