101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package node
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
)
|
|
|
|
//go:embed environment-profile.json
|
|
var environmentProfile []byte
|
|
|
|
type EnvironmentRun struct {
|
|
Schema string `json:"schema"`
|
|
ProfileRevision string `json:"profile_revision"`
|
|
RunID string `json:"run_id"`
|
|
State string `json:"state"`
|
|
StartedAt float64 `json:"started_at"`
|
|
UpdatedAt float64 `json:"updated_at"`
|
|
Steps []EnvironmentStep `json:"steps"`
|
|
}
|
|
type EnvironmentStep struct {
|
|
ID string `json:"id"`
|
|
State string `json:"state"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
type EnvironmentStatus struct {
|
|
Profile json.RawMessage `json:"profile"`
|
|
Run *EnvironmentRun `json:"run"`
|
|
Available bool `json:"available"`
|
|
}
|
|
|
|
func ReadEnvironment() EnvironmentStatus {
|
|
result := readEnvironmentFile("/var/lib/mission-core-node-environment/last-run.json")
|
|
if result.Run != nil && result.Run.State == "running" {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
state, err := exec.CommandContext(ctx, "/usr/bin/systemctl", "show", "--property=ActiveState", "--value", "mission-core-node-environment.service").Output()
|
|
if err != nil || (string(state) != "activating\n" && string(state) != "active\n") {
|
|
// A stopped job/reboot cannot leave yesterday's spinner running.
|
|
result.Run.State = "interrupted"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func readEnvironmentFile(path string) EnvironmentStatus {
|
|
result := EnvironmentStatus{Profile: json.RawMessage(environmentProfile), Available: true}
|
|
f, err := os.Open(path)
|
|
if os.IsNotExist(err) {
|
|
return result
|
|
}
|
|
if err != nil {
|
|
result.Available = false
|
|
return result
|
|
}
|
|
defer f.Close()
|
|
info, err := f.Stat()
|
|
if err != nil || !info.Mode().IsRegular() || info.Size() > 32768 {
|
|
result.Available = false
|
|
return result
|
|
}
|
|
var run EnvironmentRun
|
|
decoder := json.NewDecoder(io.LimitReader(f, 32769))
|
|
if decoder.Decode(&run) != nil || decoder.Decode(new(any)) != io.EOF || !validEnvironmentRun(run) {
|
|
result.Available = false
|
|
return result
|
|
}
|
|
result.Run = &run
|
|
return result
|
|
}
|
|
|
|
// Invalid or inconsistent progress cannot turn a failed setup into green UI.
|
|
func validEnvironmentRun(run EnvironmentRun) bool {
|
|
if run.Schema != "missioncore.node.environment/v1" || run.RunID == "" || len(run.RunID) > 64 || len(run.Steps) == 0 || len(run.Steps) > 32 || run.ProfileRevision == "" {
|
|
return false
|
|
}
|
|
if run.State != "running" && run.State != "complete" && run.State != "error" {
|
|
return false
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, step := range run.Steps {
|
|
if step.ID == "" || len(step.ID) > 64 || seen[step.ID] || len(step.Detail) > 4096 {
|
|
return false
|
|
}
|
|
seen[step.ID] = true
|
|
switch step.State {
|
|
case "pending", "running", "complete", "error", "blocked":
|
|
default:
|
|
return false
|
|
}
|
|
if run.State == "complete" && step.State != "complete" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|