From 61d793e4c644755d0f2b939dd235e15e70d3d9c3 Mon Sep 17 00:00:00 2001 From: Peng Peng Date: Tue, 21 Apr 2026 00:29:50 +0800 Subject: [PATCH] cli, daemon: add `olares-cli status` backed by olaresd /system/status (#2917) * cli, daemon: add `olares-cli status` backed by olaresd /system/status Adds a new top-level `olares-cli status` command that calls the local olaresd daemon's `/system/status` HTTP endpoint and prints either a grouped, annotated human-readable report or raw JSON. To avoid duplicating the response schema, the daemon's `state` struct and related enums are extracted into a new shared package `cli/pkg/daemon/state`. The daemon now type-aliases those types so all existing daemon call sites keep compiling unchanged. Made-with: Cursor * cli: drop unused state.APIResponse envelope type The HTTP client in cli/pkg/daemon/api/client.go intentionally uses an inline anonymous envelope with json.RawMessage for the data field so that --json mode can passthrough the bytes verbatim, so the public APIResponse{Data State} type defined here had zero references. Remove it; if a strongly-typed consumer ever shows up, re-add then. Addresses Cursor Bugbot feedback on PR #2917. Made-with: Cursor --- cli/cmd/ctl/os/root.go | 1 + cli/cmd/ctl/os/status.go | 270 ++++++++++++++++++ cli/pkg/daemon/api/client.go | 138 +++++++++ cli/pkg/daemon/state/constants.go | 202 +++++++++++++ cli/pkg/daemon/state/types.go | 234 +++++++++++++++ daemon/go.mod | 8 +- daemon/go.sum | 14 +- .../apiserver/handlers/middlewares.go | 2 +- daemon/pkg/cluster/state/current.go | 82 +----- daemon/pkg/cluster/state/self.go | 11 +- daemon/pkg/cluster/state/terminus.go | 71 ++--- daemon/pkg/commands/change_ip/cmd.go | 12 +- daemon/pkg/commands/install/cmd.go | 2 +- daemon/pkg/commands/uninstall/cmd.go | 6 +- daemon/pkg/utils/k8s.go | 2 +- daemon/pkg/utils/k8s_types.go | 12 +- docs/developer/install/cli/olares-cli.md | 1 + docs/developer/install/cli/status.md | 162 +++++++++++ docs/zh/developer/install/cli/olares-cli.md | 1 + docs/zh/developer/install/cli/status.md | 162 +++++++++++ 20 files changed, 1261 insertions(+), 132 deletions(-) create mode 100644 cli/cmd/ctl/os/status.go create mode 100644 cli/pkg/daemon/api/client.go create mode 100644 cli/pkg/daemon/state/constants.go create mode 100644 cli/pkg/daemon/state/types.go create mode 100644 docs/developer/install/cli/status.md create mode 100644 docs/zh/developer/install/cli/status.md diff --git a/cli/cmd/ctl/os/root.go b/cli/cmd/ctl/os/root.go index 6da156ec5..09b2f2fb6 100644 --- a/cli/cmd/ctl/os/root.go +++ b/cli/cmd/ctl/os/root.go @@ -19,5 +19,6 @@ func NewOSCommands() []*cobra.Command { NewCmdStart(), NewCmdStop(), NewCmdUpgradeOs(), + NewCmdStatus(), } } diff --git a/cli/cmd/ctl/os/status.go b/cli/cmd/ctl/os/status.go new file mode 100644 index 000000000..2a65abec0 --- /dev/null +++ b/cli/cmd/ctl/os/status.go @@ -0,0 +1,270 @@ +package os + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/beclab/Olares/cli/pkg/daemon/api" + "github.com/beclab/Olares/cli/pkg/daemon/state" +) + +// statusOptions holds the flags accepted by `olares-cli status`. +type statusOptions struct { + endpoint string + json bool + timeout time.Duration +} + +// NewCmdStatus returns the cobra command for `olares-cli status`. +// +// The command is a thin wrapper around the local olaresd daemon's +// /system/status endpoint. olaresd binds to 127.0.0.1:18088 and the +// endpoint is loopback-only on the daemon side, so the command must +// run on the same host as olaresd (typically the master node). +func NewCmdStatus() *cobra.Command { + opts := &statusOptions{ + endpoint: api.DefaultEndpoint, + timeout: api.DefaultTimeout, + } + + long := buildStatusLong() + + cmd := &cobra.Command{ + Use: "status", + Short: "Print the current Olares system status reported by olaresd", + Long: long, + Example: ` # Pretty-printed grouped report (default) + olares-cli status + + # Raw JSON payload (forwarded verbatim from olaresd) + olares-cli status --json | jq + + # Non-default daemon endpoint + olares-cli status --endpoint http://127.0.0.1:18088 --timeout 10s`, + Run: func(cmd *cobra.Command, args []string) { + if err := runStatus(cmd.Context(), cmd.OutOrStdout(), opts); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + }, + } + + cmd.Flags().StringVar(&opts.endpoint, "endpoint", opts.endpoint, + "Base URL of the local olaresd daemon. Override only when olaresd binds to a non-default address.") + cmd.Flags().BoolVar(&opts.json, "json", opts.json, + "Print the raw JSON payload from olaresd (the data field), suitable for piping to tools like jq.") + cmd.Flags().DurationVar(&opts.timeout, "timeout", opts.timeout, + "Maximum time to wait for the olaresd response.") + + return cmd +} + +func runStatus(ctx context.Context, out io.Writer, opts *statusOptions) error { + if ctx == nil { + ctx = context.Background() + } + + client := api.NewClient(opts.endpoint, opts.timeout) + st, raw, err := client.GetSystemStatus(ctx) + if err != nil { + return err + } + + if opts.json { + // Pretty-print to keep the output friendly when not piped. + var pretty interface{} + if jerr := json.Unmarshal(raw, &pretty); jerr == nil { + b, merr := json.MarshalIndent(pretty, "", " ") + if merr == nil { + _, err = fmt.Fprintln(out, string(b)) + return err + } + } + _, err = fmt.Fprintln(out, string(raw)) + return err + } + + return printHumanReadable(out, st) +} + +// printHumanReadable renders the State as grouped sections, each with +// padded labels and inline annotations for state values. The width +// is chosen so that values line up when printed in a typical 80-col +// terminal without truncating common content. +func printHumanReadable(out io.Writer, s *state.State) error { + const labelWidth = 20 + + w := &writer{out: out, label: labelWidth} + + w.section("Olares") + w.kv("State", string(s.TerminusState), s.TerminusState.Describe()) + w.kv("Olaresd state", string(s.TerminusdState), "") + w.kv("Name", strPtr(s.TerminusName), "") + w.kv("Version", strPtr(s.TerminusVersion), "") + w.kv("Olaresd version", strPtr(s.OlaresdVersion), "") + w.kv("Installed at", formatEpoch(s.InstalledTime), "") + w.kv("Initialized at", formatEpoch(s.InitializedTime), "") + + w.section("System") + w.kv("Device", strPtr(s.DeviceName), "") + w.kv("Hostname", strPtr(s.HostName), "") + w.kv("OS", joinOSInfo(s.OsType, s.OsArch, s.OsInfo), "") + w.kv("OS version", s.OsVersion, "") + w.kv("CPU", s.CpuInfo, "") + w.kv("Memory", s.Memory, "") + w.kv("Disk", s.Disk, "") + w.kv("GPU", strPtr(s.GpuInfo), "") + + w.section("Network") + w.kv("Wired", yesNo(s.WiredConnected), "") + w.kv("Wi-Fi", yesNo(s.WifiConnected), "") + w.kv("Wi-Fi SSID", strPtr(s.WifiSSID), "") + w.kv("Host IP", s.HostIP, "") + w.kv("External IP", s.ExternalIP, "") + + w.section("Install / Uninstall") + w.kv("Installing", string(s.InstallingState), s.InstallingProgress) + w.kv("Uninstalling", string(s.UninstallingState), s.UninstallingProgress) + + w.section("Upgrade") + w.kv("Target", s.UpgradingTarget, "") + w.kv("State", string(s.UpgradingState), s.UpgradingProgress) + w.kv("Step", s.UpgradingStep, "") + w.kv("Last error", s.UpgradingError, "") + w.kv("Download state", string(s.UpgradingDownloadState), s.UpgradingDownloadProgress) + w.kv("Download step", s.UpgradingDownloadStep, "") + w.kv("Download error", s.UpgradingDownloadError, "") + if s.UpgradingRetryNum > 0 { + w.kv("Retry count", fmt.Sprintf("%d", s.UpgradingRetryNum), "") + } + if s.UpgradingNextRetryAt != nil { + w.kv("Next retry at", s.UpgradingNextRetryAt.Local().Format(time.RFC3339), "") + } + + w.section("Logs collection") + w.kv("State", string(s.CollectingLogsState), s.CollectingLogsError) + + w.section("Pressures") + if len(s.Pressure) == 0 { + w.line("(none)") + } else { + for _, p := range s.Pressure { + w.kv(p.Type, p.Message, "") + } + } + + w.section("Other") + w.kv("FRP enabled", s.FRPEnable, "") + w.kv("FRP server", s.DefaultFRPServer, "") + w.kv("Container mode", strPtr(s.ContainerMode), "") + + return w.err +} + +// writer collects formatting helpers in one place so the section +// printer above stays declarative. +type writer struct { + out io.Writer + label int + err error +} + +func (w *writer) writef(format string, a ...interface{}) { + if w.err != nil { + return + } + _, w.err = fmt.Fprintf(w.out, format, a...) +} + +func (w *writer) section(name string) { + w.writef("\n%s\n", name) +} + +func (w *writer) line(s string) { + w.writef(" %s\n", s) +} + +func (w *writer) kv(key, value, note string) { + if value == "" { + value = "-" + } + if note != "" { + w.writef(" %-*s %s (%s)\n", w.label, key, value, note) + } else { + w.writef(" %-*s %s\n", w.label, key, value) + } +} + +func strPtr(p *string) string { + if p == nil { + return "" + } + return *p +} + +func yesNo(b bool) string { + if b { + return "yes" + } + return "no" +} + +func formatEpoch(p *int64) string { + if p == nil || *p == 0 { + return "" + } + return time.Unix(*p, 0).Local().Format("2006-01-02 15:04:05 -0700") +} + +func joinOSInfo(parts ...string) string { + out := make([]string, 0, len(parts)) + for _, p := range parts { + if p != "" { + out = append(out, p) + } + } + return strings.Join(out, " ") +} + +// buildStatusLong constructs the cobra Long help text. State +// descriptions are sourced from state.TerminusState.Describe() so +// that adding a new state here automatically updates the help. +func buildStatusLong() string { + var b strings.Builder + b.WriteString(`Print the current Olares system status reported by the local olaresd daemon. + +This command sends an HTTP GET to the daemon's /system/status endpoint +(default: http://127.0.0.1:18088/system/status). The endpoint is +loopback-only on the daemon side, so this command must run on the same +host as olaresd (typically the master node). + +The default output is a grouped, human-readable report: + + Olares installation lifecycle, version, names, key timestamps + System hardware and OS facts about the host + Network connectivity and IP addresses + Install / Uninstall progress of in-flight install or uninstall + Upgrade progress of in-flight upgrade (download + install phases) + Logs collection state of the most recent log collection job + Pressures active kubelet node pressure conditions, if any + Other FRP, container mode, etc. + +Pass --json to get the raw daemon payload instead, useful for scripting. + +Olares system states (TerminusState): + +`) + + for _, s := range state.AllTerminusStates() { + fmt.Fprintf(&b, " %-20s %s\n", string(s), s.Describe()) + } + + return b.String() +} diff --git a/cli/pkg/daemon/api/client.go b/cli/pkg/daemon/api/client.go new file mode 100644 index 000000000..43b3c97f0 --- /dev/null +++ b/cli/pkg/daemon/api/client.go @@ -0,0 +1,138 @@ +// Package api is a thin HTTP client for the local olaresd daemon. +// +// olaresd listens on TCP port 18088 by default and exposes JSON +// endpoints under paths like /system/status, /system/ifs, etc. All +// endpoints are loopback-only (RequireLocal middleware on the daemon +// side), so the client expects to talk to 127.0.0.1. +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/beclab/Olares/cli/pkg/daemon/state" +) + +// DefaultEndpoint is the loopback URL where olaresd listens by +// default. Callers normally do not need to override this. +const DefaultEndpoint = "http://127.0.0.1:18088" + +// DefaultTimeout is the per-request timeout used when the caller +// does not supply one. +const DefaultTimeout = 5 * time.Second + +// Client is a small HTTP client for the olaresd daemon. The zero +// value uses DefaultEndpoint and DefaultTimeout; callers may +// override either field before issuing requests. +type Client struct { + // Endpoint is the base URL of olaresd, e.g. + // "http://127.0.0.1:18088". Trailing slashes are stripped. + Endpoint string + + // Timeout bounds the total time spent on a single HTTP request, + // including dialing, TLS, and reading the response body. + Timeout time.Duration + + // HTTPClient lets tests substitute a custom transport. When + // nil, the client constructs one on demand using Timeout. + HTTPClient *http.Client +} + +// NewClient returns a Client that hits the given endpoint with the +// supplied timeout. Pass an empty endpoint or zero timeout to fall +// back to DefaultEndpoint / DefaultTimeout. +func NewClient(endpoint string, timeout time.Duration) *Client { + return &Client{Endpoint: endpoint, Timeout: timeout} +} + +func (c *Client) endpoint() string { + ep := c.Endpoint + if ep == "" { + ep = DefaultEndpoint + } + return strings.TrimRight(ep, "/") +} + +func (c *Client) httpClient() *http.Client { + if c.HTTPClient != nil { + return c.HTTPClient + } + t := c.Timeout + if t <= 0 { + t = DefaultTimeout + } + return &http.Client{Timeout: t} +} + +// GetSystemStatus calls GET /system/status and returns: +// - the parsed State struct for programmatic consumption; +// - the raw `data` JSON bytes so callers that want to forward +// the response (for example, the CLI's --json mode) can do so +// without a re-marshal round trip. +// +// The error message always contains the endpoint URL and the HTTP +// status (when applicable) so users can tell the difference between +// "olaresd is down" and "olaresd returned an error". +func (c *Client) GetSystemStatus(ctx context.Context) (*state.State, []byte, error) { + u, err := url.Parse(c.endpoint()) + if err != nil { + return nil, nil, fmt.Errorf("invalid olaresd endpoint %q: %w", c.endpoint(), err) + } + u.Path = "/system/status" + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, nil, fmt.Errorf("build request to %s: %w", u.String(), err) + } + req.Header.Set("Accept", "application/json") + + resp, err := c.httpClient().Do(req) + if err != nil { + return nil, nil, fmt.Errorf("request olaresd at %s (is olaresd running?): %w", u.String(), err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("read response from %s: %w", u.String(), err) + } + + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("olaresd %s returned HTTP %d: %s", u.String(), resp.StatusCode, truncate(string(body), 200)) + } + + // olaresd wraps payloads as {code, message, data}. We need the + // raw `data` slice both to populate State and to expose it + // verbatim to --json callers. + var raw struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(body, &raw); err != nil { + return nil, nil, fmt.Errorf("decode response envelope from %s: %w", u.String(), err) + } + if raw.Code != http.StatusOK { + return nil, nil, fmt.Errorf("olaresd returned code=%d message=%q", raw.Code, raw.Message) + } + + var s state.State + if err := json.Unmarshal(raw.Data, &s); err != nil { + return nil, nil, fmt.Errorf("decode state payload from %s: %w", u.String(), err) + } + + return &s, []byte(raw.Data), nil +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/cli/pkg/daemon/state/constants.go b/cli/pkg/daemon/state/constants.go new file mode 100644 index 000000000..bf1091744 --- /dev/null +++ b/cli/pkg/daemon/state/constants.go @@ -0,0 +1,202 @@ +package state + +// TerminusDState is the lifecycle state of the olaresd daemon process. +type TerminusDState string + +const ( + // Initialize means olaresd has just started and is still + // bootstrapping its watchers and configuration. + Initialize TerminusDState = "initialize" + + // Running means olaresd has finished initialization and is + // serving requests normally. + Running TerminusDState = "running" +) + +// ProcessingState is the lifecycle of a long-running operation that +// olaresd reports progress for (install, uninstall, upgrade, log +// collection). +type ProcessingState string + +const ( + // Completed means the operation finished successfully. + Completed ProcessingState = "completed" + + // Failed means the operation finished with an error. Inspect the + // associated *Error field for details. + Failed ProcessingState = "failed" + + // InProgress means the operation is currently running. + InProgress ProcessingState = "in-progress" +) + +// TerminusState is the high-level state machine value for the Olares +// system as observed from this node. Use Describe() to obtain a +// human-readable, one-line summary suitable for end-user output. +type TerminusState string + +const ( + // NotInstalled means Olares is not installed on this node. + NotInstalled TerminusState = "not-installed" + + // Installing means an installation is currently in progress. + Installing TerminusState = "installing" + + // InstallFailed means the most recent installation attempt + // failed. Re-run install or uninstall to recover. + InstallFailed TerminusState = "install-failed" + + // Uninitialized means Olares is installed but the admin user has + // not completed initial activation yet. + Uninitialized TerminusState = "uninitialized" + + // Initializing means the admin user is going through the initial + // activation flow. + Initializing TerminusState = "initializing" + + // InitializeFailed means the initial activation failed. + InitializeFailed TerminusState = "initialize-failed" + + // TerminusRunning means Olares is fully installed, activated, + // and all key pods are healthy. + TerminusRunning TerminusState = "terminus-running" + + // InvalidIpAddress means the node's IP has changed since + // installation; run change-ip to fix it. + InvalidIpAddress TerminusState = "invalid-ip-address" + + // SystemError means one or more critical pods are not running, + // or the cluster API is unreachable. + SystemError TerminusState = "system-error" + + // SelfRepairing means olaresd is automatically attempting to + // recover from a system error. + SelfRepairing TerminusState = "self-repairing" + + // IPChanging means a change-ip operation is currently running. + IPChanging TerminusState = "ip-changing" + + // IPChangeFailed means the most recent change-ip attempt failed. + IPChangeFailed TerminusState = "ip-change-failed" + + // AddingNode means a worker node is currently being joined. + AddingNode TerminusState = "adding-node" + + // RemovingNode means a worker node is currently being removed. + RemovingNode TerminusState = "removing-node" + + // Uninstalling means an uninstall is currently in progress. + Uninstalling TerminusState = "uninstalling" + + // Upgrading means an upgrade install phase is currently running. + // The dedicated download phase does not flip TerminusState. + Upgrading TerminusState = "upgrading" + + // DiskModifing means a storage reconfiguration is in progress. + DiskModifing TerminusState = "disk-modifing" + + // Shutdown means the system is in the process of shutting down. + Shutdown TerminusState = "shutdown" + + // Restarting means the node has been up for less than the + // stabilization window (3 minutes for healthy systems, 10 for + // degraded ones), so reported pod state may be stale. + Restarting TerminusState = "restarting" + + // Checking means olaresd has not yet completed the first status + // probe. This is the default value before WatchStatus runs. + Checking TerminusState = "checking" + + // NetworkNotReady means no usable internal IPv4 address was + // detected on this node. + NetworkNotReady TerminusState = "network-not-ready" +) + +// String returns the wire value of the state, allowing TerminusState +// to satisfy fmt.Stringer. +func (s TerminusState) String() string { + return string(s) +} + +// Describe returns a one-line, end-user oriented explanation of the +// state value. Empty values are rendered as "unknown state". Unknown +// values are returned as-is so the CLI can still display them. +func (s TerminusState) Describe() string { + switch s { + case NotInstalled: + return "Olares is not installed on this node" + case Installing: + return "Olares is currently being installed" + case InstallFailed: + return "the most recent install attempt failed" + case Uninitialized: + return "Olares is installed but the admin user has not been activated yet" + case Initializing: + return "the admin user activation is in progress" + case InitializeFailed: + return "the admin user activation failed" + case TerminusRunning: + return "Olares is running normally" + case InvalidIpAddress: + return "the node IP changed since install; run change-ip to recover" + case SystemError: + return "one or more critical pods are not running" + case SelfRepairing: + return "olaresd is attempting automatic recovery" + case IPChanging: + return "a change-ip operation is in progress" + case IPChangeFailed: + return "the most recent change-ip attempt failed" + case AddingNode: + return "a worker node is being joined" + case RemovingNode: + return "a worker node is being removed" + case Uninstalling: + return "Olares is being uninstalled" + case Upgrading: + return "an upgrade is being applied" + case DiskModifing: + return "the storage layout is being modified" + case Shutdown: + return "the system is shutting down" + case Restarting: + return "the node was just restarted, status will stabilize shortly" + case Checking: + return "olaresd has not finished the first status probe yet" + case NetworkNotReady: + return "no usable internal IPv4 address detected" + case "": + return "unknown state" + default: + return string(s) + } +} + +// AllTerminusStates returns the full list of TerminusState values in +// a stable, documentation-friendly order. It is used by the CLI's +// long help text and by the docs generator. +func AllTerminusStates() []TerminusState { + return []TerminusState{ + Checking, + NetworkNotReady, + NotInstalled, + Installing, + InstallFailed, + Uninitialized, + Initializing, + InitializeFailed, + TerminusRunning, + Restarting, + InvalidIpAddress, + IPChanging, + IPChangeFailed, + SystemError, + SelfRepairing, + AddingNode, + RemovingNode, + Uninstalling, + Upgrading, + DiskModifing, + Shutdown, + } +} diff --git a/cli/pkg/daemon/state/types.go b/cli/pkg/daemon/state/types.go new file mode 100644 index 000000000..0beb1482c --- /dev/null +++ b/cli/pkg/daemon/state/types.go @@ -0,0 +1,234 @@ +// Package state defines the data types exposed by the local olaresd +// daemon's GET /system/status endpoint. +// +// These types are intentionally placed in the cli module so that both +// the olaresd daemon and the olares-cli command line tool can share +// the same wire format. The daemon (which already imports this module +// via its go.mod) re-exports these types as aliases, while the CLI +// uses them directly to unmarshal the HTTP response. +// +// Only data types belong here. Business logic that depends on +// daemon-internal packages (state validators, status probing, etc.) +// must remain in the daemon module. +package state + +import "time" + +// State is the full system status snapshot maintained by olaresd. +// It is refreshed every 5s by the daemon's status watcher and served +// as the `data` field of the GET /system/status response. +// +// All fields use JSON tags that match the wire format byte for byte; +// do not rename JSON keys without updating every consumer (CLI, +// frontend, mDNS clients, etc.). +type State struct { + // TerminusdState is the lifecycle state of the olaresd daemon + // itself. Possible values: "initialize" (just started, still + // bootstrapping) or "running" (fully initialized). + TerminusdState TerminusDState `json:"terminusdState"` + + // TerminusState is the high-level state of the Olares system. + // It drives both UI display and command admission control. See + // the TerminusState constants below for the full enumeration + // and call Describe() to obtain a one-line explanation. + TerminusState TerminusState `json:"terminusState"` + + // TerminusName is the Olares ID of the admin user, e.g. + // "alice@olares.cn". It is read from the local release file when + // available and refreshed from the cluster once Olares is up. + TerminusName *string `json:"terminusName,omitempty"` + + // TerminusVersion is the installed Olares version (semver), e.g. + // "1.12.0". + TerminusVersion *string `json:"terminusVersion,omitempty"` + + // InstalledTime is the Unix epoch (seconds) at which Olares + // finished installing on this node. Nil before install completes. + InstalledTime *int64 `json:"installedTime,omitempty"` + + // InitializedTime is the Unix epoch (seconds) at which the admin + // user finished the initial activation. Nil before activation. + InitializedTime *int64 `json:"initializedTime,omitempty"` + + // OlaresdVersion is the running olaresd binary version. Useful + // for diagnosing version drift between olaresd and the rest of + // Olares after a partial upgrade. + OlaresdVersion *string `json:"olaresdVersion,omitempty"` + + // InstallFinishedTime is daemon-internal: the wall clock time at + // which the most recent install finished. Used to derive + // InstalledTime when the cluster is not reachable yet. Excluded + // from the wire format. + InstallFinishedTime *time.Time `json:"-"` + + // DeviceName is the user-friendly device name (model / chassis + // name) detected from the host. + DeviceName *string `json:"device_name,omitempty"` + + // HostName is the kernel hostname of the node. + HostName *string `json:"host_name,omitempty"` + + // OsType is the OS family, e.g. "linux" or "darwin". + OsType string `json:"os_type"` + + // OsArch is the CPU architecture, e.g. "amd64" or "arm64". + OsArch string `json:"os_arch"` + + // OsInfo is a human-readable OS distribution string, e.g. + // "Ubuntu 22.04". + OsInfo string `json:"os_info"` + + // OsVersion is the OS version string, e.g. "22.04". + OsVersion string `json:"os_version"` + + // CpuInfo is the CPU model name as reported by the OS. + CpuInfo string `json:"cpu_info"` + + // GpuInfo is the GPU model name when one is detected. + GpuInfo *string `json:"gpu_info,omitempty"` + + // Memory is the total physical memory, formatted as " G". + Memory string `json:"memory"` + + // Disk is the total filesystem size of the node's data partition, + // formatted as " G". + Disk string `json:"disk"` + + // WifiConnected is true when the active default route is over + // Wi-Fi. The JSON key is "wifiConnected". + WifiConnected bool `json:"wifiConnected"` + + // WifiSSID is the SSID of the connected Wi-Fi network, when + // WifiConnected is true. + WifiSSID *string `json:"wifiSSID,omitempty"` + + // WiredConnected is true when the node has an active Ethernet + // connection. + WiredConnected bool `json:"wiredConnected"` + + // HostIP is the internal LAN IPv4 address that Olares uses to + // register itself in /etc/hosts and to reach other nodes. + HostIP string `json:"hostIp"` + + // ExternalIP is the public IPv4 address as observed by an + // external probe. Refreshed at most once per minute. + ExternalIP string `json:"externalIp"` + + // ExternalIPProbeTime is daemon-internal: when the external IP + // probe last ran. Excluded from the wire format. + ExternalIPProbeTime time.Time `json:"-"` + + // InstallingState reports the progress of an installation in + // flight: "in-progress", "completed", "failed", or empty. + InstallingState ProcessingState `json:"installingState"` + + // InstallingProgress is a free-form human-readable description + // of the current installation step. + InstallingProgress string `json:"installingProgress"` + + // InstallingProgressNum is daemon-internal: the latest numeric + // install progress percentage. Excluded from the wire format. + InstallingProgressNum int `json:"-"` + + // UninstallingState mirrors InstallingState for the uninstall + // flow. + UninstallingState ProcessingState `json:"uninstallingState"` + + // UninstallingProgress is a free-form description of the + // current uninstall step. + UninstallingProgress string `json:"uninstallingProgress"` + + // UninstallingProgressNum is daemon-internal: the latest numeric + // uninstall progress percentage. Excluded from the wire format. + UninstallingProgressNum int `json:"-"` + + // UpgradingTarget is the target version of the in-flight + // upgrade, e.g. "1.13.0". Empty when no upgrade is queued. + UpgradingTarget string `json:"upgradingTarget"` + + // UpgradingRetryNum is the number of times the upgrader has + // retried after a transient failure. + UpgradingRetryNum int `json:"upgradingRetryNum"` + + // UpgradingNextRetryAt is the wall-clock time at which the next + // retry will fire, when retries are pending. + UpgradingNextRetryAt *time.Time `json:"upgradingNextRetryAt,omitempty"` + + // UpgradingState is the lifecycle of the install phase of the + // upgrade ("in-progress", "completed", "failed", or empty). + UpgradingState ProcessingState `json:"upgradingState"` + + // UpgradingStep is the name of the current upgrade step. + UpgradingStep string `json:"upgradingStep"` + + // UpgradingProgress is the free-form progress message for the + // current upgrade step. + UpgradingProgress string `json:"upgradingProgress"` + + // UpgradingProgressNum is daemon-internal: the latest numeric + // upgrade progress percentage. Excluded from the wire format. + UpgradingProgressNum int `json:"-"` + + // UpgradingError is the most recent error seen during upgrade. + // Empty when no error has occurred. + UpgradingError string `json:"upgradingError"` + + // UpgradingDownloadState is the lifecycle of the download phase + // of the upgrade. Olares splits download and install into two + // phases so that downloads can complete in the background + // without changing TerminusState to "upgrading". + UpgradingDownloadState ProcessingState `json:"upgradingDownloadState"` + + // UpgradingDownloadStep is the name of the current download step. + UpgradingDownloadStep string `json:"upgradingDownloadStep"` + + // UpgradingDownloadProgress is the free-form progress message + // for the current download step. + UpgradingDownloadProgress string `json:"upgradingDownloadProgress"` + + // UpgradingDownloadProgressNum is daemon-internal: the latest + // numeric download progress percentage. Excluded from the wire + // format. + UpgradingDownloadProgressNum int `json:"-"` + + // UpgradingDownloadError is the most recent error from the + // download phase. Empty when no error has occurred. + UpgradingDownloadError string `json:"upgradingDownloadError"` + + // CollectingLogsState is the lifecycle of the most recent log + // collection job triggered through olaresd. + CollectingLogsState ProcessingState `json:"collectingLogsState"` + + // CollectingLogsError is the error from the most recent log + // collection job, when it failed. + CollectingLogsError string `json:"collectingLogsError"` + + // DefaultFRPServer is the FRP server address used when frp is + // enabled. Sourced from the FRP_SERVER env var. + DefaultFRPServer string `json:"defaultFrpServer"` + + // FRPEnable indicates whether the FRP-based reverse tunnel is + // turned on. Sourced from the FRP_ENABLE env var. + FRPEnable string `json:"frpEnable"` + + // ContainerMode is set when olaresd is running inside a + // container, mirroring the CONTAINER_MODE env var. + ContainerMode *string `json:"containerMode,omitempty"` + + // Pressure lists the kubernetes node-condition pressures + // currently active on this node (memory pressure, disk pressure, + // PID pressure, etc.). Empty when the node is healthy. + Pressure []NodePressure `json:"pressures,omitempty"` +} + +// NodePressure represents a non-Ready kubernetes node condition that +// is currently true on this node, e.g. MemoryPressure, DiskPressure, +// PIDPressure, NetworkUnavailable. +type NodePressure struct { + // Type is the kubernetes node condition type, e.g. + // "MemoryPressure". + Type string `json:"type"` + + // Message is the human-readable explanation provided by kubelet. + Message string `json:"message"` +} \ No newline at end of file diff --git a/daemon/go.mod b/daemon/go.mod index e313d5da2..04394f9de 100644 --- a/daemon/go.mod +++ b/daemon/go.mod @@ -5,13 +5,13 @@ go 1.24.11 replace ( bytetrade.io/web3os/backups-sdk => github.com/Above-Os/backups-sdk v0.1.17 bytetrade.io/web3os/bfl => github.com/beclab/bfl v0.3.36 + github.com/beclab/Olares/cli => ../cli github.com/labstack/echo/v4 => github.com/eball/echo/v4 v4.13.4-patch k8s.io/api => k8s.io/api v0.34.0 k8s.io/apimachinery => k8s.io/apimachinery v0.34.0 k8s.io/client-go => k8s.io/client-go v0.34.0 kubesphere.io/api => ../../kubesphere-ext/staging/src/kubesphere.io/api/ sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.19.6 - ) require ( @@ -19,7 +19,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/beclab/Olares/cli v0.0.0-20251230161135-5264df60cc33 - github.com/beclab/Olares/framework/app-service v0.0.0-20251225061130-909b7656fd70 + github.com/beclab/Olares/framework/app-service v0.0.0-20260311124303-23a6533bc2ad github.com/containerd/containerd v1.7.29 github.com/distribution/distribution/v3 v3.0.0 github.com/dustin/go-humanize v1.0.1 @@ -46,7 +46,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible github.com/shirou/gopsutil/v4 v4.25.7 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/pflag v1.0.7 + github.com/spf13/pflag v1.0.10 github.com/txn2/txeh v1.5.5 github.com/vishvananda/netlink v1.3.0 go.opentelemetry.io/otel/trace v1.40.0 @@ -173,7 +173,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.40.0 // indirect diff --git a/daemon/go.sum b/daemon/go.sum index f5b6c46d8..f8de82598 100644 --- a/daemon/go.sum +++ b/daemon/go.sum @@ -24,10 +24,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/beclab/Olares/cli v0.0.0-20251230161135-5264df60cc33 h1:WYuUPOT/p26aCDJGJEDai1v7YM6QHiaFDusBVynnbBY= -github.com/beclab/Olares/cli v0.0.0-20251230161135-5264df60cc33/go.mod h1:ixhzBK5XIovsRB5djk44TChsOK4wum2q4y/hZxJKlNw= -github.com/beclab/Olares/framework/app-service v0.0.0-20251225061130-909b7656fd70 h1:U3z6m0hokD1gzl788BrUdxCbDyAjdOBBXA8ilYgn6VQ= -github.com/beclab/Olares/framework/app-service v0.0.0-20251225061130-909b7656fd70/go.mod h1:D9wl7y3obLqXMqfubMROMgdxWAwInnKNrFC//d0nyIA= +github.com/beclab/Olares/framework/app-service v0.0.0-20260311124303-23a6533bc2ad h1:nmQCNbJNtgTqcusySeeyd9LQOK2jyk78QAjzmvyyPYg= +github.com/beclab/Olares/framework/app-service v0.0.0-20260311124303-23a6533bc2ad/go.mod h1:D9wl7y3obLqXMqfubMROMgdxWAwInnKNrFC//d0nyIA= github.com/beclab/bfl v0.3.36 h1:PgeSPGc+XoONiwFsKq9xX8rqcL4kVM1G/ut0lYYj/js= github.com/beclab/bfl v0.3.36/go.mod h1:A82u38MxYk1C3Lqnm4iUUK4hBeY9HHIs+xU4V93OnJk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -331,8 +329,8 @@ github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE= github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4= github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -386,8 +384,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6h go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= diff --git a/daemon/internel/apiserver/handlers/middlewares.go b/daemon/internel/apiserver/handlers/middlewares.go index 7864e5b43..96b2348f3 100644 --- a/daemon/internel/apiserver/handlers/middlewares.go +++ b/daemon/internel/apiserver/handlers/middlewares.go @@ -87,7 +87,7 @@ func (h *Handlers) RunCommand(next func(ctx *fiber.Ctx, cmd commands.Interface) return func(ctx *fiber.Ctx) error { c := cmdNew() - err := state.CurrentState.TerminusState.ValidateOp(c) + err := state.ValidateOp(state.CurrentState.TerminusState, c) if err != nil { return h.ErrJSON(ctx, http.StatusForbidden, err.Error()) } diff --git a/daemon/pkg/cluster/state/current.go b/daemon/pkg/cluster/state/current.go index 76bf0b182..af7601a7d 100644 --- a/daemon/pkg/cluster/state/current.go +++ b/daemon/pkg/cluster/state/current.go @@ -9,6 +9,7 @@ import ( "sync" "time" + clistate "github.com/beclab/Olares/cli/pkg/daemon/state" "github.com/beclab/Olares/daemon/internel/watcher" "github.com/beclab/Olares/daemon/pkg/commands" "github.com/beclab/Olares/daemon/pkg/nets" @@ -19,68 +20,11 @@ import ( "github.com/pbnjay/memory" ) -type state struct { - TerminusdState TerminusDState `json:"terminusdState"` - TerminusState TerminusState `json:"terminusState"` - TerminusName *string `json:"terminusName,omitempty"` - TerminusVersion *string `json:"terminusVersion,omitempty"` - InstalledTime *int64 `json:"installedTime,omitempty"` - InitializedTime *int64 `json:"initializedTime,omitempty"` - OlaresdVersion *string `json:"olaresdVersion,omitempty"` - InstallFinishedTime *time.Time `json:"-"` - - // sys info - DeviceName *string `json:"device_name,omitempty"` - HostName *string `json:"host_name,omitempty"` - OsType string `json:"os_type"` - OsArch string `json:"os_arch"` - OsInfo string `json:"os_info"` - OsVersion string `json:"os_version"` - CpuInfo string `json:"cpu_info"` - GpuInfo *string `json:"gpu_info,omitempty"` - Memory string `json:"memory"` - Disk string `json:"disk"` - - // network info - WikiConnected bool `json:"wifiConnected"` - WifiSSID *string `json:"wifiSSID,omitempty"` - WiredConnected bool `json:"wiredConnected"` - HostIP string `json:"hostIp"` - ExternalIP string `json:"externalIp"` - ExternalIPProbeTime time.Time `json:"-"` - - // installing / uninstalling / upgrading state - InstallingState ProcessingState `json:"installingState"` - InstallingProgress string `json:"installingProgress"` - InstallingProgressNum int `json:"-"` - UninstallingState ProcessingState `json:"uninstallingState"` - UninstallingProgress string `json:"uninstallingProgress"` - UninstallingProgressNum int `json:"-"` - UpgradingTarget string `json:"upgradingTarget"` - UpgradingRetryNum int `json:"upgradingRetryNum"` - UpgradingNextRetryAt *time.Time `json:"upgradingNextRetryAt,omitempty"` - UpgradingState ProcessingState `json:"upgradingState"` - UpgradingStep string `json:"upgradingStep"` - UpgradingProgress string `json:"upgradingProgress"` - UpgradingProgressNum int `json:"-"` - UpgradingError string `json:"upgradingError"` - - UpgradingDownloadState ProcessingState `json:"upgradingDownloadState"` - UpgradingDownloadStep string `json:"upgradingDownloadStep"` - UpgradingDownloadProgress string `json:"upgradingDownloadProgress"` - UpgradingDownloadProgressNum int `json:"-"` - UpgradingDownloadError string `json:"upgradingDownloadError"` - - CollectingLogsState ProcessingState `json:"collectingLogsState"` - CollectingLogsError string `json:"collectingLogsError"` - - DefaultFRPServer string `json:"defaultFrpServer"` - FRPEnable string `json:"frpEnable"` - - ContainerMode *string `json:"containerMode,omitempty"` - - Pressure []utils.NodePressure `json:"pressures,omitempty"` -} +// state is a daemon-local alias of the canonical wire-format struct +// defined in cli/pkg/daemon/state. Keeping it as an alias means +// existing daemon code keeps using the same field names without +// changes, while the CLI shares the exact same data type. +type state = clistate.State var CurrentState state var StateTrigger chan struct{} @@ -92,11 +36,15 @@ func init() { StateTrigger = make(chan struct{}) } -func (c *state) ChangeTerminusStateTo(s TerminusState) { +// ChangeTerminusStateTo updates the global TerminusState under the +// shared mutex. It used to be a method on state, but methods cannot +// be defined on a type alias whose underlying type lives in another +// package, so it is now a package-level function. +func ChangeTerminusStateTo(s TerminusState) { TerminusStateMu.Lock() defer TerminusStateMu.Unlock() - c.TerminusState = s + CurrentState.TerminusState = s } func bToGb(b uint64) string { @@ -193,7 +141,7 @@ func CheckCurrentStatus(ctx context.Context) error { } // clear value - CurrentState.WikiConnected = false + CurrentState.WifiConnected = false CurrentState.WifiSSID = nil CurrentState.WiredConnected = false for _, i := range ips { @@ -201,7 +149,7 @@ func CheckCurrentStatus(ctx context.Context) error { if d, ok := devices[i.Iface.Name]; ok { switch d.Type { case "wifi": - CurrentState.WikiConnected = true + CurrentState.WifiConnected = true CurrentState.WifiSSID = &d.Connection case "ethernet": CurrentState.WiredConnected = true @@ -212,7 +160,7 @@ func CheckCurrentStatus(ctx context.Context) error { // for macos if strings.HasPrefix(i.Iface.Name, "wl") { - CurrentState.WikiConnected = true + CurrentState.WifiConnected = true CurrentState.WifiSSID = utils.WifiName() } diff --git a/daemon/pkg/cluster/state/self.go b/daemon/pkg/cluster/state/self.go index c5d21bed9..6ad75d341 100644 --- a/daemon/pkg/cluster/state/self.go +++ b/daemon/pkg/cluster/state/self.go @@ -1,8 +1,13 @@ package state -type TerminusDState string +import clistate "github.com/beclab/Olares/cli/pkg/daemon/state" + +// TerminusDState is the lifecycle state of olaresd itself. The +// canonical definition lives in the cli module and is re-exported +// here as an alias so the daemon can use the unqualified name. +type TerminusDState = clistate.TerminusDState const ( - Initialize TerminusDState = "initialize" - Running TerminusDState = "running" + Initialize = clistate.Initialize + Running = clistate.Running ) diff --git a/daemon/pkg/cluster/state/terminus.go b/daemon/pkg/cluster/state/terminus.go index 0312314aa..b8b78e740 100644 --- a/daemon/pkg/cluster/state/terminus.go +++ b/daemon/pkg/cluster/state/terminus.go @@ -3,52 +3,57 @@ package state import ( "errors" + clistate "github.com/beclab/Olares/cli/pkg/daemon/state" "github.com/beclab/Olares/daemon/pkg/commands" ) -type ProcessingState string +// Wire-format types live in the cli module so that olaresd and the +// olares-cli command share a single source of truth. Daemon code keeps +// using the unqualified names below via these type aliases. + +type ProcessingState = clistate.ProcessingState const ( - Completed ProcessingState = "completed" - Failed ProcessingState = "failed" - InProgress ProcessingState = "in-progress" + Completed = clistate.Completed + Failed = clistate.Failed + InProgress = clistate.InProgress ) -type TerminusState string +type TerminusState = clistate.TerminusState const ( - NotInstalled TerminusState = "not-installed" - Installing TerminusState = "installing" - InstallFailed TerminusState = "install-failed" - Uninitialized TerminusState = "uninitialized" - Initializing TerminusState = "initializing" - InitializeFailed TerminusState = "initialize-failed" - TerminusRunning TerminusState = "terminus-running" - InvalidIpAddress TerminusState = "invalid-ip-address" - SystemError TerminusState = "system-error" - SelfRepairing TerminusState = "self-repairing" - IPChanging TerminusState = "ip-changing" - IPChangeFailed TerminusState = "ip-change-failed" - AddingNode TerminusState = "adding-node" - RemovingNode TerminusState = "removing-node" - Uninstalling TerminusState = "uninstalling" - Upgrading TerminusState = "upgrading" - DiskModifing TerminusState = "disk-modifing" - Shutdown TerminusState = "shutdown" - Restarting TerminusState = "restarting" - Checking TerminusState = "checking" - NetworkNotReady TerminusState = "network-not-ready" + NotInstalled = clistate.NotInstalled + Installing = clistate.Installing + InstallFailed = clistate.InstallFailed + Uninitialized = clistate.Uninitialized + Initializing = clistate.Initializing + InitializeFailed = clistate.InitializeFailed + TerminusRunning = clistate.TerminusRunning + InvalidIpAddress = clistate.InvalidIpAddress + SystemError = clistate.SystemError + SelfRepairing = clistate.SelfRepairing + IPChanging = clistate.IPChanging + IPChangeFailed = clistate.IPChangeFailed + AddingNode = clistate.AddingNode + RemovingNode = clistate.RemovingNode + Uninstalling = clistate.Uninstalling + Upgrading = clistate.Upgrading + DiskModifing = clistate.DiskModifing + Shutdown = clistate.Shutdown + Restarting = clistate.Restarting + Checking = clistate.Checking + NetworkNotReady = clistate.NetworkNotReady ) -func (s TerminusState) String() string { - return string(s) +// ValidateOp returns nil if the operation is allowed in the given +// state, or a descriptive error otherwise. It replaces the previous +// (TerminusState).ValidateOp method, since methods cannot be defined +// on an alias of a type from another package. +func ValidateOp(s TerminusState, op commands.Interface) error { + return getValidator(s).ValidateOp(op) } -func (s TerminusState) ValidateOp(op commands.Interface) error { - return s.getValidator().ValidateOp(op) -} - -func (s TerminusState) getValidator() Validator { +func getValidator(s TerminusState) Validator { switch s { case NotInstalled: return &NotInstalledValidator{} diff --git a/daemon/pkg/commands/change_ip/cmd.go b/daemon/pkg/commands/change_ip/cmd.go index 2bc41f274..7962eb52b 100644 --- a/daemon/pkg/commands/change_ip/cmd.go +++ b/daemon/pkg/commands/change_ip/cmd.go @@ -75,7 +75,7 @@ func (i *changeIp) Execute(ctx context.Context, p any) (res any, err error) { klog.Error("cannot backup prev ip, ", err) } - state.CurrentState.ChangeTerminusStateTo(state.IPChanging) + state.ChangeTerminusStateTo(state.IPChanging) // remove PREV_IP_CHANGE_FAILED tag file if exists if _, err = os.Stat(commands.PREV_IP_CHANGE_FAILED); err == nil { @@ -114,7 +114,7 @@ func (i *changeIp) watch(ctx context.Context) { // double check if i.tailLog() { klog.Info("change ip command finished, change state") - state.CurrentState.ChangeTerminusStateTo(successState) + state.ChangeTerminusStateTo(successState) return } else { klog.Warning("check log file, process not succeed") @@ -123,12 +123,12 @@ func (i *changeIp) watch(ctx context.Context) { } } klog.Warning("change ip killed") - state.CurrentState.ChangeTerminusStateTo(state.SystemError) + state.ChangeTerminusStateTo(state.SystemError) return case <-ticker.C: if i.tailLog() { klog.Info("watch log succeed") - state.CurrentState.ChangeTerminusStateTo(successState) + state.ChangeTerminusStateTo(successState) return } } @@ -175,7 +175,7 @@ func (i *changeIp) tailLog() (finished bool) { func (i *changeIp) setFailedState() { if i.isRetryChange { klog.Error("retry ip change failed") - state.CurrentState.ChangeTerminusStateTo(state.SystemError) + state.ChangeTerminusStateTo(state.SystemError) } else { // create a ip change failed tag file, make the ip-change command can // be resumed if the device get reboot @@ -184,6 +184,6 @@ func (i *changeIp) setFailedState() { klog.Error("write ip change failed tag file error, ", err) } - state.CurrentState.ChangeTerminusStateTo(state.IPChangeFailed) + state.ChangeTerminusStateTo(state.IPChangeFailed) } } diff --git a/daemon/pkg/commands/install/cmd.go b/daemon/pkg/commands/install/cmd.go index 973760faf..9ddbb6759 100644 --- a/daemon/pkg/commands/install/cmd.go +++ b/daemon/pkg/commands/install/cmd.go @@ -92,7 +92,7 @@ func (i *install) watch(ctx context.Context) { return } else { state.CurrentState.InstallingState = state.Failed - state.CurrentState.ChangeTerminusStateTo(state.InstallFailed) + state.ChangeTerminusStateTo(state.InstallFailed) } } return diff --git a/daemon/pkg/commands/uninstall/cmd.go b/daemon/pkg/commands/uninstall/cmd.go index 92b708426..c0f21a9f0 100644 --- a/daemon/pkg/commands/uninstall/cmd.go +++ b/daemon/pkg/commands/uninstall/cmd.go @@ -45,7 +45,7 @@ func (i *uninstall) Execute(ctx context.Context, p any) (res any, err error) { return nil, err } - state.CurrentState.ChangeTerminusStateTo(state.Uninstalling) + state.ChangeTerminusStateTo(state.Uninstalling) state.CurrentState.UninstallingState = state.InProgress state.CurrentState.UninstallingProgress = "1%" state.CurrentState.UninstallingProgressNum = 1 @@ -67,7 +67,7 @@ func (i *uninstall) watch(ctx context.Context) { } if !installed { - state.CurrentState.ChangeTerminusStateTo(state.NotInstalled) + state.ChangeTerminusStateTo(state.NotInstalled) state.CurrentState.UninstallingState = state.Completed state.CurrentState.UninstallingProgress = "100%" state.CurrentState.UninstallingProgressNum = 100 @@ -124,7 +124,7 @@ func (i *uninstall) tailLog() (finished bool) { state.CurrentState.UninstallingState = state.InProgress } else { state.CurrentState.UninstallingState = state.Completed - state.CurrentState.ChangeTerminusStateTo(state.NotInstalled) + state.ChangeTerminusStateTo(state.NotInstalled) return true } } diff --git a/daemon/pkg/utils/k8s.go b/daemon/pkg/utils/k8s.go index fb70e11f1..e647b94b4 100644 --- a/daemon/pkg/utils/k8s.go +++ b/daemon/pkg/utils/k8s.go @@ -541,7 +541,7 @@ func GetNodesPressure(ctx context.Context, client kubernetes.Interface) (map[str for _, node := range nodes.Items { for _, condition := range node.Status.Conditions { if condition.Type != corev1.NodeReady && condition.Status == corev1.ConditionTrue { - status[node.Name] = append(status[node.Name], NodePressure{Type: condition.Type, Message: condition.Message}) + status[node.Name] = append(status[node.Name], NodePressure{Type: string(condition.Type), Message: condition.Message}) } } } diff --git a/daemon/pkg/utils/k8s_types.go b/daemon/pkg/utils/k8s_types.go index 0362d94c7..8d5ede19a 100644 --- a/daemon/pkg/utils/k8s_types.go +++ b/daemon/pkg/utils/k8s_types.go @@ -1,7 +1,7 @@ package utils import ( - corev1 "k8s.io/api/core/v1" + clistate "github.com/beclab/Olares/cli/pkg/daemon/state" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -15,7 +15,9 @@ var ( } ) -type NodePressure struct { - Type corev1.NodeConditionType `json:"type"` - Message string `json:"message"` -} +// NodePressure is a daemon-local alias of the canonical wire type +// shared with the olares-cli module. The Type field is plain string +// rather than corev1.NodeConditionType because the JSON wire format +// uses a string anyway and the cli module avoids depending on +// k8s.io/api/core/v1. +type NodePressure = clistate.NodePressure diff --git a/docs/developer/install/cli/olares-cli.md b/docs/developer/install/cli/olares-cli.md index 2f92ede65..1f2c76d2c 100644 --- a/docs/developer/install/cli/olares-cli.md +++ b/docs/developer/install/cli/olares-cli.md @@ -60,6 +60,7 @@ To get detailed help for any command, run `olares-cli help`. | `prepare` | `olares-cli prepare [options]` | Prepares the environment for the installation process, including setting up essential services and configurations of Olares. | | `release` | `olares-cli release [options]` | Packages Olares installation resources for distribution or deployment. | | `start` | `olares-cli start [options]` | Starts Olares services and components. | +| `status` | `olares-cli status [options]` | Queries the local olaresd daemon and prints the current Olares system status. | | `stop` | `olares-cli stop [options]` | Stops Olares services and components. | | `uninstall` | `olares-cli uninstall [options]` | Uninstalls Olares completely, or roll back the installation to a specific phase. | | `upgrade` | `olares-cli upgrade [options]` | Upgrades Olares and checks upgrade readiness and compatibility.| diff --git a/docs/developer/install/cli/status.md b/docs/developer/install/cli/status.md new file mode 100644 index 000000000..818b00384 --- /dev/null +++ b/docs/developer/install/cli/status.md @@ -0,0 +1,162 @@ +# `status` + +## Synopsis + +The `status` command prints the current Olares system state by calling the local olaresd daemon's `/system/status` HTTP endpoint. + +The endpoint is bound to `127.0.0.1:18088` and only accepts loopback traffic, so this command must run on the same host as the daemon (typically the master node). + +```bash +olares-cli status [options] +``` + +By default, the output is grouped into human-readable sections: + +- **Olares**: installation lifecycle, version, names, key timestamps. +- **System**: hardware and OS facts about the host. +- **Network**: wired/Wi-Fi connectivity, internal and external IP addresses. +- **Install / Uninstall**: progress of an in-flight install or uninstall. +- **Upgrade**: progress of an in-flight upgrade (download and install phases). +- **Logs collection**: state of the most recent log collection job. +- **Pressures**: active kubelet node pressure conditions, if any. +- **Other**: FRP, container mode, etc. + +Use `--json` to receive the raw payload returned by olaresd, suitable for scripting or piping to tools like `jq`. + +## Options + +| Option | Usage | Required | Default | +|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------------| +| `--endpoint` | Base URL of the local olaresd daemon. Override only when olaresd is bound to a non-default address. | No | `http://127.0.0.1:18088` | +| `--json` | Print the raw JSON payload returned by olaresd (the `data` field), suitable for piping to tools like `jq`. | No | `false` | +| `--timeout` | Maximum time to wait for the olaresd response. | No | `5s` | +| `--help` | Show command help. | No | N/A | + +## Examples + +```bash +# Pretty-printed grouped report (default) +olares-cli status + +# Raw JSON payload (forwarded verbatim from olaresd) +olares-cli status --json | jq + +# Custom daemon endpoint and longer timeout +olares-cli status --endpoint http://127.0.0.1:18088 --timeout 10s +``` + +## Field reference + +The table below lists the fields returned by `olaresd` (the `data` object of the JSON response) and the labels they appear under in the grouped output. + +### Olares + +| Field | JSON key | Meaning | +|-------------------|--------------------|----------------------------------------------------------------------------------------------------------| +| State | `terminusState` | High-level system state. See [State values](#state-values). | +| Olaresd state | `terminusdState` | Lifecycle of the olaresd daemon itself: `initialize` while bootstrapping, `running` once ready. | +| Name | `terminusName` | Olares ID of the admin user, e.g. `alice@olares.cn`. | +| Version | `terminusVersion` | Installed Olares version (semver). | +| Olaresd version | `olaresdVersion` | Running olaresd binary version. Useful for spotting drift after partial upgrades. | +| Installed at | `installedTime` | Unix epoch (seconds) when Olares finished installing on this node. | +| Initialized at | `initializedTime` | Unix epoch (seconds) when the admin user finished initial activation. | + +### System + +| Field | JSON key | Meaning | +|-------------|---------------|----------------------------------------------------------------------| +| Device | `device_name` | User-friendly device or chassis name. | +| Hostname | `host_name` | Kernel hostname. | +| OS | `os_type` / `os_arch` / `os_info` | OS family, CPU architecture, distro string. | +| OS version | `os_version` | OS version string, e.g. `22.04`. | +| CPU | `cpu_info` | CPU model name. | +| Memory | `memory` | Total physical memory, formatted as ` G`. | +| Disk | `disk` | Total filesystem size of the data partition, formatted as ` G`. | +| GPU | `gpu_info` | GPU model name when one is detected. | + +### Network + +| Field | JSON key | Meaning | +|-------------|------------------|----------------------------------------------------------------------| +| Wired | `wiredConnected` | `yes` when an Ethernet connection is active. | +| Wi-Fi | `wifiConnected` | `yes` when the active default route is over Wi-Fi. | +| Wi-Fi SSID | `wifiSSID` | SSID of the connected Wi-Fi network, when applicable. | +| Host IP | `hostIp` | Internal LAN IPv4 address used by Olares to reach other nodes. | +| External IP | `externalIp` | Public IPv4 address as observed by an external probe (refreshed at most once per minute). | + +### Install / Uninstall + +| Field | JSON key | Meaning | +|----------------|-------------------------|-------------------------------------------------------------------------------| +| Installing | `installingState` | Lifecycle of the in-flight install: `in-progress`, `completed`, `failed`. | +| | `installingProgress` | Free-form description of the current install step (shown as the inline note). | +| Uninstalling | `uninstallingState` | Lifecycle of the in-flight uninstall. | +| | `uninstallingProgress` | Free-form description of the current uninstall step. | + +### Upgrade + +| Field | JSON key | Meaning | +|----------------|--------------------------------|-----------------------------------------------------------------------------------| +| Target | `upgradingTarget` | Target version of the in-flight upgrade. | +| State | `upgradingState` | Lifecycle of the install phase of the upgrade. | +| | `upgradingProgress` | Free-form progress message (inline note). | +| Step | `upgradingStep` | Name of the current upgrade step. | +| Last error | `upgradingError` | Most recent error from the install phase. | +| Download state | `upgradingDownloadState` | Lifecycle of the download phase. | +| | `upgradingDownloadProgress` | Free-form progress message for the download (inline note). | +| Download step | `upgradingDownloadStep` | Name of the current download step. | +| Download error | `upgradingDownloadError` | Most recent error from the download phase. | +| Retry count | `upgradingRetryNum` | Number of times the upgrader has retried after a transient failure (only shown when > 0). | +| Next retry at | `upgradingNextRetryAt` | Wall-clock time at which the next retry will fire (only shown when set). | + +### Logs collection + +| Field | JSON key | Meaning | +|-------------|--------------------------|------------------------------------------------------------------| +| State | `collectingLogsState` | Lifecycle of the most recent log collection job triggered through olaresd. | +| | `collectingLogsError` | Error from the most recent log collection job (inline note). | + +### Pressures + +The `pressures` array lists kubelet node-condition pressures that are currently true on this node. The grouped output shows `(none)` when the node is healthy. + +| Field | JSON key | Meaning | +|---------|-----------|------------------------------------------------------------------------| +| Type | `type` | Kubernetes node condition type, e.g. `MemoryPressure`, `DiskPressure`. | +| Message | `message` | Human-readable explanation provided by kubelet. | + +### Other + +| Field | JSON key | Meaning | +|----------------|---------------------|------------------------------------------------------------------------| +| FRP enabled | `frpEnable` | Whether the FRP-based reverse tunnel is turned on (sourced from `FRP_ENABLE`). | +| FRP server | `defaultFrpServer` | FRP server address (sourced from `FRP_SERVER`). | +| Container mode | `containerMode` | Set when olaresd is running inside a container (sourced from `CONTAINER_MODE`). | + +## State values + +The `terminusState` field can take one of the following values. The CLI sources its descriptions from the same enumeration, so the table below stays in sync with the CLI output. + +| Value | Meaning | +|----------------------|--------------------------------------------------------------------------| +| `checking` | olaresd has not finished the first status probe yet. | +| `network-not-ready` | No usable internal IPv4 address detected. | +| `not-installed` | Olares is not installed on this node. | +| `installing` | Olares is currently being installed. | +| `install-failed` | The most recent install attempt failed. | +| `uninitialized` | Olares is installed but the admin user has not been activated yet. | +| `initializing` | The admin user activation is in progress. | +| `initialize-failed` | The admin user activation failed. | +| `terminus-running` | Olares is running normally. | +| `restarting` | The node was just restarted; status will stabilize shortly. | +| `invalid-ip-address` | The node IP changed since install; run `change-ip` to recover. | +| `ip-changing` | A `change-ip` operation is in progress. | +| `ip-change-failed` | The most recent `change-ip` attempt failed. | +| `system-error` | One or more critical pods are not running. | +| `self-repairing` | olaresd is attempting automatic recovery. | +| `adding-node` | A worker node is being joined. | +| `removing-node` | A worker node is being removed. | +| `uninstalling` | Olares is being uninstalled. | +| `upgrading` | An upgrade is being applied. | +| `disk-modifing` | The storage layout is being modified. | +| `shutdown` | The system is shutting down. | diff --git a/docs/zh/developer/install/cli/olares-cli.md b/docs/zh/developer/install/cli/olares-cli.md index 025a92173..8c36eea95 100644 --- a/docs/zh/developer/install/cli/olares-cli.md +++ b/docs/zh/developer/install/cli/olares-cli.md @@ -60,6 +60,7 @@ Olares 命令行工具使用如下语法: | `prepare` | `olares-cli prepare [选项]` | 为安装过程准备环境,包括设置 Olares 的基础服务和配置。 | | `release` | `olares-cli release [选项]` | 打包 Olares 安装资源以供分发或部署。| | `start` | `olares-cli start [选项]` | 启动 Olares 服务和组件。 | +| `status` | `olares-cli status [选项]` | 查询本机 olaresd 守护进程,输出当前 Olares 系统状态。 | | `stop` | `olares-cli stop [选项]` | 停止 Olares 服务和组件。 | | `uninstall` | `olares-cli uninstall [选项]` | 完全卸载 Olares,或将安装回滚到特定阶段。 | | `upgrade` | `olares-cli upgrade <子命令> [选项]` | 升级 Olares,检查升级准备情况与兼容性。 | diff --git a/docs/zh/developer/install/cli/status.md b/docs/zh/developer/install/cli/status.md new file mode 100644 index 000000000..45dbdf1aa --- /dev/null +++ b/docs/zh/developer/install/cli/status.md @@ -0,0 +1,162 @@ +# `status` + +## 命令说明 + +`status` 命令通过调用本机 olaresd 守护进程的 `/system/status` HTTP 接口,输出当前 Olares 系统的状态。 + +该接口绑定在 `127.0.0.1:18088`,仅接受本地回环流量,因此 `status` 命令必须与 olaresd 运行在同一台机器(通常是主节点)上。 + +```bash +olares-cli status [选项] +``` + +默认输出按以下分组展示,便于人工阅读: + +- **Olares**:安装生命周期、版本、用户名、关键时间戳。 +- **System**:主机的硬件和操作系统信息。 +- **Network**:有线/Wi-Fi 连接状态、内/外网 IP 地址。 +- **Install / Uninstall**:正在进行的安装或卸载进度。 +- **Upgrade**:正在进行的升级进度(包括下载阶段和安装阶段)。 +- **Logs collection**:最近一次日志收集任务的状态。 +- **Pressures**:节点上当前激活的 kubelet 节点压力条件(若有)。 +- **Other**:FRP、容器模式等其他信息。 + +加上 `--json` 可以输出 olaresd 返回的原始 JSON,便于脚本化处理或与 `jq` 等工具配合使用。 + +## 选项 + +| 选项 | 用途 | 是否必需 | 默认值 | +|--------------|----------------------------------------------------------------------------------------------|----------|--------------------------| +| `--endpoint` | 本机 olaresd 守护进程的基础 URL。仅当 olaresd 监听在非默认地址时才需要修改。 | 否 | `http://127.0.0.1:18088` | +| `--json` | 直接输出 olaresd 返回的原始 JSON(即响应中的 `data` 字段),适合配合 `jq` 等工具使用。 | 否 | `false` | +| `--timeout` | 等待 olaresd 响应的最长时间。 | 否 | `5s` | +| `--help` | 显示命令帮助。 | 否 | 无 | + +## 使用示例 + +```bash +# 默认输出:分组的人工可读报表 +olares-cli status + +# 原始 JSON 输出,原样转发自 olaresd +olares-cli status --json | jq + +# 指定守护进程地址并延长超时时间 +olares-cli status --endpoint http://127.0.0.1:18088 --timeout 10s +``` + +## 字段参考 + +下表列出 olaresd 返回的字段(即 JSON 响应中 `data` 对象的字段),以及它们在分组输出中显示的标签。 + +### Olares + +| 字段 | JSON Key | 含义 | +|------------------|--------------------|--------------------------------------------------------------------------------------------| +| State | `terminusState` | 系统的高层状态,详见 [状态值列表](#状态值列表)。 | +| Olaresd state | `terminusdState` | olaresd 守护进程自身的生命周期:启动初始化时为 `initialize`,初始化完成后为 `running`。 | +| Name | `terminusName` | 管理员的 Olares ID,例如 `alice@olares.cn`。 | +| Version | `terminusVersion` | 已安装的 Olares 版本(语义化版本号)。 | +| Olaresd version | `olaresdVersion` | 当前运行的 olaresd 二进制版本。可用于排查升级后的版本漂移。 | +| Installed at | `installedTime` | Olares 安装完成时间(Unix 时间戳,单位秒)。 | +| Initialized at | `initializedTime` | 管理员完成初始激活的时间(Unix 时间戳,单位秒)。 | + +### System + +| 字段 | JSON Key | 含义 | +|------------|---------------|--------------------------------------------| +| Device | `device_name` | 用户友好的设备/机型名称。 | +| Hostname | `host_name` | 内核报告的主机名。 | +| OS | `os_type` / `os_arch` / `os_info` | 操作系统类型、CPU 架构、发行版描述。 | +| OS version | `os_version` | 操作系统版本号,例如 `22.04`。 | +| CPU | `cpu_info` | CPU 型号。 | +| Memory | `memory` | 物理内存总量,格式为 ` G`。 | +| Disk | `disk` | 数据分区的文件系统总容量,格式为 ` G`。 | +| GPU | `gpu_info` | 检测到的 GPU 型号(若有)。 | + +### Network + +| 字段 | JSON Key | 含义 | +|-------------|------------------|---------------------------------------------------------------------| +| Wired | `wiredConnected` | 检测到有线连接时为 `yes`。 | +| Wi-Fi | `wifiConnected` | 默认路由走 Wi-Fi 时为 `yes`。 | +| Wi-Fi SSID | `wifiSSID` | 已连接 Wi-Fi 的 SSID。 | +| Host IP | `hostIp` | Olares 用于互联的内网 IPv4 地址。 | +| External IP | `externalIp` | 通过外部探测获取的公网 IPv4 地址(每分钟最多刷新一次)。 | + +### Install / Uninstall + +| 字段 | JSON Key | 含义 | +|---------------|-------------------------|-------------------------------------------------------------------| +| Installing | `installingState` | 进行中的安装任务的生命周期:`in-progress`、`completed`、`failed`。 | +| | `installingProgress` | 当前安装步骤的描述(在分组输出中以括号形式跟随显示)。 | +| Uninstalling | `uninstallingState` | 进行中的卸载任务的生命周期。 | +| | `uninstallingProgress` | 当前卸载步骤的描述。 | + +### Upgrade + +| 字段 | JSON Key | 含义 | +|----------------|--------------------------------|----------------------------------------------------------------------------| +| Target | `upgradingTarget` | 进行中升级的目标版本。 | +| State | `upgradingState` | 升级安装阶段的生命周期。 | +| | `upgradingProgress` | 升级安装阶段的进度描述(括号显示)。 | +| Step | `upgradingStep` | 当前升级步骤的名称。 | +| Last error | `upgradingError` | 升级安装阶段最近一次报错信息。 | +| Download state | `upgradingDownloadState` | 升级下载阶段的生命周期。 | +| | `upgradingDownloadProgress` | 升级下载阶段的进度描述(括号显示)。 | +| Download step | `upgradingDownloadStep` | 当前下载步骤的名称。 | +| Download error | `upgradingDownloadError` | 升级下载阶段最近一次报错信息。 | +| Retry count | `upgradingRetryNum` | 升级被自动重试的次数(仅当大于 0 时显示)。 | +| Next retry at | `upgradingNextRetryAt` | 下一次重试的预定时间(仅当存在时显示)。 | + +### Logs collection + +| 字段 | JSON Key | 含义 | +|------------|--------------------------|-----------------------------------------------------------------------| +| State | `collectingLogsState` | 通过 olaresd 触发的最近一次日志收集任务的生命周期。 | +| | `collectingLogsError` | 最近一次日志收集任务的错误信息(括号显示)。 | + +### Pressures + +`pressures` 数组列出当前节点上为真的 kubelet 节点压力条件。当节点健康时,分组输出中会显示 `(none)`。 + +| 字段 | JSON Key | 含义 | +|---------|-----------|---------------------------------------------------------------------| +| Type | `type` | Kubernetes 节点条件类型,例如 `MemoryPressure`、`DiskPressure`。 | +| Message | `message` | kubelet 给出的可读说明。 | + +### Other + +| 字段 | JSON Key | 含义 | +|----------------|---------------------|---------------------------------------------------------------------| +| FRP enabled | `frpEnable` | FRP 反向通道是否启用(来自环境变量 `FRP_ENABLE`)。 | +| FRP server | `defaultFrpServer` | FRP 服务器地址(来自环境变量 `FRP_SERVER`)。 | +| Container mode | `containerMode` | olaresd 运行在容器内时设置(来自环境变量 `CONTAINER_MODE`)。 | + +## 状态值列表 + +`terminusState` 字段可能取以下值。CLI 也使用同一份枚举生成描述,因此下表始终与 CLI 输出保持一致。 + +| 取值 | 含义 | +|-----------------------|-----------------------------------------------------------------| +| `checking` | olaresd 还未完成首次状态探测。 | +| `network-not-ready` | 未检测到可用的内网 IPv4 地址。 | +| `not-installed` | 当前节点未安装 Olares。 | +| `installing` | Olares 正在安装中。 | +| `install-failed` | 最近一次安装失败。 | +| `uninitialized` | Olares 已安装,但管理员账户尚未激活。 | +| `initializing` | 管理员账户正在激活中。 | +| `initialize-failed` | 管理员账户激活失败。 | +| `terminus-running` | Olares 运行正常。 | +| `restarting` | 节点刚刚重启,状态会在短时间内稳定。 | +| `invalid-ip-address` | 节点 IP 已变更,需要执行 `change-ip` 恢复。 | +| `ip-changing` | `change-ip` 操作正在进行。 | +| `ip-change-failed` | 最近一次 `change-ip` 操作失败。 | +| `system-error` | 关键 Pod 未正常运行。 | +| `self-repairing` | olaresd 正在尝试自动修复。 | +| `adding-node` | 正在加入 worker 节点。 | +| `removing-node` | 正在移除 worker 节点。 | +| `uninstalling` | Olares 正在卸载中。 | +| `upgrading` | 升级正在执行中。 | +| `disk-modifing` | 存储布局正在调整中。 | +| `shutdown` | 系统正在关机。 |