Files
mesh-host/internal/processbuilds/processbuilds.go
T

141 lines
4.6 KiB
Go

// Package processbuilds reads the build each process of this node was started with (design 47, *The fingerprint handover*):
// the value MESH_FINGERPRINT the node-engine handed a daemon or a container at its start, read from the running process
// itself, never from what the declaration would hand it now. A process started before the node-engine handed the value,
// or one whose value cannot be read, says why its build is unknown instead of a fingerprint.
package processbuilds
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/declaration"
)
// Var is the environment value the build is handed in.
const Var = "MESH_FINGERPRINT"
// Runner runs a command and answers its output.
type Runner func(ctx context.Context, name string, args ...string) (string, error)
// Reader reads the running processes of a node.
type Reader struct {
Node string
Run Runner
// Environ reads a process's environment by its pid; nil reads /proc/<pid>/environ.
Environ func(pid int) ([]byte, error)
}
func (r Reader) environ(pid int) ([]byte, error) {
if r.Environ != nil {
return r.Environ(pid)
}
return os.ReadFile(fmt.Sprintf("/proc/%d/environ", pid))
}
// Read answers the build of every daemon and container the declaration holds, narrowed to module when it is not empty,
// ordered by module and process.
func (r Reader) Read(ctx context.Context, d *declaration.Declaration, module string) nodeengine.ProcessBuilds {
out := nodeengine.ProcessBuilds{Node: r.Node, Processes: []nodeengine.ProcessBuild{}}
if d == nil {
out.Error = "this node keeps no declaration of what it runs"
return out
}
for _, res := range d.Resources {
owner := res.DeclaredModule()
if owner == "" {
owner, _ = declaration.ModuleGuess(res.Identity())
}
if module != "" && owner != module {
continue
}
switch p := res.(type) {
case *declaration.Process:
out.Processes = append(out.Processes, r.daemon(ctx, owner, p.Name))
case *declaration.Container:
out.Processes = append(out.Processes, r.container(ctx, owner, p.Name))
}
}
sort.SliceStable(out.Processes, func(i, j int) bool {
a, b := out.Processes[i], out.Processes[j]
if a.Module != b.Module {
return a.Module < b.Module
}
return a.Process < b.Process
})
return out
}
func (r Reader) daemon(ctx context.Context, module, name string) nodeengine.ProcessBuild {
unit := name + ".service"
b := nodeengine.ProcessBuild{Module: module, Process: unit, Kind: nodeengine.ProcessBuildKindService}
said, err := r.Run(ctx, "systemctl", "show", "--property=MainPID,ActiveEnterTimestamp", "--", unit)
if err != nil {
b.Unknown = "the unit cannot be read: " + err.Error()
return b
}
props := map[string]string{}
for _, line := range strings.Split(said, "\n") {
if k, v, ok := strings.Cut(strings.TrimSpace(line), "="); ok {
props[k] = v
}
}
pid, _ := strconv.Atoi(props["MainPID"])
if pid <= 0 {
b.Unknown = "the unit runs no process now"
return b
}
if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", props["ActiveEnterTimestamp"]); err == nil {
b.Started = &t
}
raw, err := r.environ(pid)
if err != nil {
b.Unknown = "its environment cannot be read: " + err.Error()
return b
}
b.Fingerprint, b.Unknown = fingerprintIn(strings.Split(string(raw), "\x00"))
return b
}
func (r Reader) container(ctx context.Context, module, name string) nodeengine.ProcessBuild {
b := nodeengine.ProcessBuild{Module: module, Process: name, Kind: nodeengine.ProcessBuildKindContainer}
var said string
var err error
for _, cri := range []string{"docker", "podman"} {
if said, err = r.Run(ctx, cri, "container", "inspect", "--format", "{{json .Config.Env}}\t{{.State.StartedAt}}", name); err == nil {
break
}
}
if err != nil {
b.Unknown = "the container cannot be read: " + err.Error()
return b
}
env, started, _ := strings.Cut(strings.TrimSpace(said), "\t")
var vars []string
if err := json.Unmarshal([]byte(env), &vars); err != nil {
b.Unknown = "its environment cannot be read: " + err.Error()
return b
}
if t, err := time.Parse(time.RFC3339Nano, started); err == nil {
b.Started = &t
}
b.Fingerprint, b.Unknown = fingerprintIn(vars)
return b
}
// fingerprintIn is the value of the handed variable in a process's environment, or why there is none.
func fingerprintIn(vars []string) (fingerprint, unknown string) {
for _, v := range vars {
if value, ok := strings.CutPrefix(v, Var+"="); ok && value != "" {
return value, ""
}
}
return "", "started before the node-engine handed " + Var + ", or without it"
}