runtime(metis): move remote workspace onto usb scratch

This commit is contained in:
codex 2026-04-23 23:36:42 -03:00
parent ca1e3a5f74
commit ef8c2131a6
9 changed files with 217 additions and 91 deletions

View File

@ -23,7 +23,7 @@ import (
func remoteDevicesCmd(args []string) { func remoteDevicesCmd(args []string) {
fs := flag.NewFlagSet("remote-devices", flag.ExitOnError) fs := flag.NewFlagSet("remote-devices", flag.ExitOnError)
maxBytes := fs.Int64("max-device-bytes", 300000000000, "max real removable device size") maxBytes := fs.Int64("max-device-bytes", 300000000000, "max real removable device size")
hostTmpDir := fs.String("host-tmp-dir", "/tmp/metis-flash-test", "host tmp dir for test writes") hostTmpDir := fs.String("host-tmp-dir", "/var/tmp/metis-flash-test", "host tmp dir for test writes")
fs.Parse(args) fs.Parse(args)
devices, err := localFlashDevices(*maxBytes, *hostTmpDir) devices, err := localFlashDevices(*maxBytes, *hostTmpDir)
@ -166,7 +166,7 @@ func remoteFlashCmd(args []string) {
harborRegistry := fs.String("harbor-registry", getenvOr("METIS_HARBOR_REGISTRY", "registry.bstein.dev"), "harbor registry host") harborRegistry := fs.String("harbor-registry", getenvOr("METIS_HARBOR_REGISTRY", "registry.bstein.dev"), "harbor registry host")
harborUsername := fs.String("harbor-username", getenvOr("METIS_HARBOR_USERNAME", ""), "harbor username") harborUsername := fs.String("harbor-username", getenvOr("METIS_HARBOR_USERNAME", ""), "harbor username")
harborPassword := fs.String("harbor-password", getenvOr("METIS_HARBOR_PASSWORD", ""), "harbor password") harborPassword := fs.String("harbor-password", getenvOr("METIS_HARBOR_PASSWORD", ""), "harbor password")
hostTmpDir := fs.String("host-tmp-dir", "/host-tmp/metis-flash-test", "mounted host tmp dir for test writes") hostTmpDir := fs.String("host-tmp-dir", "/host-tmp", "mounted host tmp dir for test writes")
fs.Parse(args) fs.Parse(args)
if *node == "" || *device == "" || *artifactRef == "" { if *node == "" || *device == "" || *artifactRef == "" {
fatalf("--node, --device, and --artifact-ref are required") fatalf("--node, --device, and --artifact-ref are required")
@ -349,13 +349,17 @@ func localFlashDevices(maxBytes int64, hostTmpDir string) ([]service.Device, err
SizeBytes: size, SizeBytes: size,
}) })
} }
displayPath := humanHostPath(hostTmpDir)
if strings.TrimSpace(displayPath) == "" {
displayPath = "/var/tmp/metis-flash-test"
}
devices = append(devices, service.Device{ devices = append(devices, service.Device{
Name: "host-tmp", Name: "host-tmp",
Path: "hosttmp:///tmp", Path: "hosttmp://" + displayPath,
Model: "Host /tmp", Model: "Host scratch",
Transport: "test", Transport: "test",
Type: "file", Type: "file",
Note: fmt.Sprintf("Test-only host write target under %s", humanHostPath(hostTmpDir)), Note: fmt.Sprintf("Test-only host write target under %s", displayPath),
Removable: false, Removable: false,
Hotplug: false, Hotplug: false,
SizeBytes: 1, SizeBytes: 1,

View File

@ -16,12 +16,14 @@ import (
) )
type clusterNode struct { type clusterNode struct {
Name string Name string
Arch string Arch string
Hardware string Hardware string
Worker bool Worker bool
ControlPlane bool ControlPlane bool
Unschedulable bool Unschedulable bool
USBScratchStatus string
USBScratchManagedPaths string
} }
type podState struct { type podState struct {
@ -171,8 +173,9 @@ func clusterNodes() []clusterNode {
var payload struct { var payload struct {
Items []struct { Items []struct {
Metadata struct { Metadata struct {
Name string `json:"name"` Name string `json:"name"`
Labels map[string]string `json:"labels"` Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata"` } `json:"metadata"`
Spec struct { Spec struct {
Unschedulable bool `json:"unschedulable"` Unschedulable bool `json:"unschedulable"`
@ -185,13 +188,16 @@ func clusterNodes() []clusterNode {
nodes := make([]clusterNode, 0, len(payload.Items)) nodes := make([]clusterNode, 0, len(payload.Items))
for _, item := range payload.Items { for _, item := range payload.Items {
labels := item.Metadata.Labels labels := item.Metadata.Labels
annotations := item.Metadata.Annotations
nodes = append(nodes, clusterNode{ nodes = append(nodes, clusterNode{
Name: strings.TrimSpace(item.Metadata.Name), Name: strings.TrimSpace(item.Metadata.Name),
Arch: strings.TrimSpace(labels["kubernetes.io/arch"]), Arch: strings.TrimSpace(labels["kubernetes.io/arch"]),
Hardware: strings.TrimSpace(labels["hardware"]), Hardware: strings.TrimSpace(labels["hardware"]),
Worker: labels["node-role.kubernetes.io/worker"] == "true", Worker: labels["node-role.kubernetes.io/worker"] == "true",
ControlPlane: labels["node-role.kubernetes.io/control-plane"] != "" || labels["node-role.kubernetes.io/master"] != "", ControlPlane: labels["node-role.kubernetes.io/control-plane"] != "" || labels["node-role.kubernetes.io/master"] != "",
Unschedulable: item.Spec.Unschedulable, Unschedulable: item.Spec.Unschedulable,
USBScratchStatus: strings.TrimSpace(annotations["maintenance.bstein.dev/usb-scratch-status"]),
USBScratchManagedPaths: strings.TrimSpace(annotations["maintenance.bstein.dev/usb-scratch-managed-paths"]),
}) })
} }
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Name < nodes[j].Name }) sort.Slice(nodes, func(i, j int) bool { return nodes[i].Name < nodes[j].Name })

View File

@ -9,7 +9,7 @@ import (
) )
const ( const (
hostTmpDevicePath = "hosttmp:///tmp" hostTmpDevicePath = "hosttmp:///var/tmp/metis-flash-test"
vaultRoleMaintenance = "maintenance" vaultRoleMaintenance = "maintenance"
vaultRuntimeSecretPath = "kv/data/atlas/maintenance/metis-runtime" vaultRuntimeSecretPath = "kv/data/atlas/maintenance/metis-runtime"
vaultHarborSecretPath = "kv/data/atlas/harbor/harbor-core" vaultHarborSecretPath = "kv/data/atlas/harbor/harbor-core"

View File

@ -337,11 +337,11 @@ func remoteTestApp(t *testing.T, harbor *httptest.Server) *App {
func remoteWorkflowKubeServer(t *testing.T, opts remoteKubeOptions) *httptest.Server { func remoteWorkflowKubeServer(t *testing.T, opts remoteKubeOptions) *httptest.Server {
t.Helper() t.Helper()
devicePhase := defaultString(opts.devicePhase, "Succeeded") devicePhase := defaultString(opts.devicePhase, "Succeeded")
deviceMessage := defaultString(opts.deviceMessage, `{"devices":[{"name":"sdz","path":"/dev/sdz","model":"Micro SD","transport":"usb","type":"disk","removable":true,"hotplug":true,"size_bytes":32000000000},{"name":"tmp","path":"hosttmp:///tmp","model":"Host /tmp","transport":"test","type":"file","note":"Test-only host write target under /tmp","size_bytes":1}]}`) deviceMessage := defaultString(opts.deviceMessage, `{"devices":[{"name":"sdz","path":"/dev/sdz","model":"Micro SD","transport":"usb","type":"disk","removable":true,"hotplug":true,"size_bytes":32000000000},{"name":"tmp","path":"hosttmp:///var/tmp/metis-flash-test","model":"Host scratch","transport":"test","type":"file","note":"Test-only host write target under /var/tmp/metis-flash-test","size_bytes":1}]}`)
buildPhase := defaultString(opts.buildPhase, "Succeeded") buildPhase := defaultString(opts.buildPhase, "Succeeded")
buildMessage := defaultString(opts.buildMessage, `{"local_path":"/workspace/build/titan-15.img.xz","compressed":true,"size_bytes":1234,"build_tag":"build-1"}`) buildMessage := defaultString(opts.buildMessage, `{"local_path":"/workspace/build/titan-15.img.xz","compressed":true,"size_bytes":1234,"build_tag":"build-1"}`)
flashPhase := defaultString(opts.flashPhase, "Succeeded") flashPhase := defaultString(opts.flashPhase, "Succeeded")
flashMessage := defaultString(opts.flashMessage, `{"dest_path":"/tmp/metis-flash-test/titan-15.img"}`) flashMessage := defaultString(opts.flashMessage, `{"dest_path":"/var/tmp/metis-flash-test/titan-15.img"}`)
nodes := opts.nodes nodes := opts.nodes
if nodes == nil { if nodes == nil {
nodes = []map[string]any{ nodes = []map[string]any{

View File

@ -42,7 +42,7 @@ func flashStageHeartbeat(host, artifact string, elapsed time.Duration) (float64,
func prettyDeviceTarget(path string) string { func prettyDeviceTarget(path string) string {
switch { switch {
case strings.HasPrefix(path, "hosttmp://"): case strings.HasPrefix(path, "hosttmp://"):
return "/tmp" return strings.TrimPrefix(path, "hosttmp://")
case strings.TrimSpace(path) == "": case strings.TrimSpace(path) == "":
return "the selected target" return "the selected target"
default: default:
@ -50,6 +50,42 @@ func prettyDeviceTarget(path string) string {
} }
} }
func hostTmpHostPath(path string) string {
clean := filepath.Clean(strings.TrimSpace(path))
if clean == "" || clean == "." || clean == "/" {
return "/var/tmp/metis-flash-test"
}
return clean
}
func remoteWorkspaceHostPath(root, podName string) string {
cleanRoot := filepath.Clean(strings.TrimSpace(root))
if cleanRoot == "" || cleanRoot == "." || cleanRoot == "/" {
cleanRoot = "/var/tmp/metis-workspace"
}
if strings.TrimSpace(podName) == "" {
return cleanRoot
}
return filepath.Join(cleanRoot, podName)
}
func managedPathsContain(raw, want string) bool {
want = strings.TrimSpace(want)
if want == "" {
return false
}
for _, path := range strings.Split(raw, "_") {
if strings.TrimSpace(path) == want {
return true
}
}
return false
}
func usbScratchReadyForWorkspace(node clusterNode) bool {
return node.USBScratchStatus == "ok" && managedPathsContain(node.USBScratchManagedPaths, "/var/tmp")
}
func ramp(value, start, end, min, max float64) float64 { func ramp(value, start, end, min, max float64) float64 {
if end <= start { if end <= start {
return max return max
@ -107,6 +143,13 @@ func (a *App) selectBuilderHost(arch, flashHost string) (clusterNode, error) {
if node.Hardware == "rpi5" { if node.Hardware == "rpi5" {
score += 30 score += 30
} }
if usbScratchReadyForWorkspace(node) {
score += 120
} else if node.USBScratchStatus == "error" {
score -= 200
} else {
score -= 80
}
if _, storage := storageNodes[node.Name]; storage { if _, storage := storageNodes[node.Name]; storage {
score -= 50 score -= 50
} }
@ -164,14 +207,13 @@ func (a *App) remoteDevicePodSpec(name, host, image string) map[string]any {
"command": []string{ "command": []string{
"metis", "remote-devices", "metis", "remote-devices",
"--max-device-bytes", fmt.Sprintf("%d", a.settings.MaxDeviceBytes), "--max-device-bytes", fmt.Sprintf("%d", a.settings.MaxDeviceBytes),
"--host-tmp-dir", mountedHostTmpDir(a.settings.HostTmpDir), "--host-tmp-dir", hostTmpHostPath(a.settings.HostTmpDir),
}, },
"securityContext": map[string]any{"privileged": true, "runAsUser": 0}, "securityContext": map[string]any{"privileged": true, "runAsUser": 0},
"volumeMounts": []map[string]any{ "volumeMounts": []map[string]any{
{"name": "host-dev", "mountPath": "/dev"}, {"name": "host-dev", "mountPath": "/dev"},
{"name": "host-sys", "mountPath": "/sys", "readOnly": true}, {"name": "host-sys", "mountPath": "/sys", "readOnly": true},
{"name": "host-udev", "mountPath": "/run/udev", "readOnly": true}, {"name": "host-udev", "mountPath": "/run/udev", "readOnly": true},
{"name": "host-tmp", "mountPath": "/host-tmp"},
}, },
}, },
}, },
@ -180,13 +222,13 @@ func (a *App) remoteDevicePodSpec(name, host, image string) map[string]any {
{"name": "host-dev", "hostPath": map[string]any{"path": "/dev"}}, {"name": "host-dev", "hostPath": map[string]any{"path": "/dev"}},
{"name": "host-sys", "hostPath": map[string]any{"path": "/sys"}}, {"name": "host-sys", "hostPath": map[string]any{"path": "/sys"}},
{"name": "host-udev", "hostPath": map[string]any{"path": "/run/udev"}}, {"name": "host-udev", "hostPath": map[string]any{"path": "/run/udev"}},
{"name": "host-tmp", "hostPath": map[string]any{"path": "/tmp"}},
}, },
}, },
} }
} }
func (a *App) remoteBuildPodSpec(name, host, image, node, artifactRef, buildTag string) map[string]any { func (a *App) remoteBuildPodSpec(name, host, image, node, artifactRef, buildTag string) map[string]any {
workspaceHostPath := remoteWorkspaceHostPath(a.settings.RemoteWorkspaceDir, name)
return map[string]any{ return map[string]any{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
@ -231,13 +273,15 @@ func (a *App) remoteBuildPodSpec(name, host, image, node, artifactRef, buildTag
}, },
"imagePullSecrets": []map[string]string{{"name": "harbor-regcred"}}, "imagePullSecrets": []map[string]string{{"name": "harbor-regcred"}},
"volumes": []map[string]any{ "volumes": []map[string]any{
{"name": "workspace", "emptyDir": map[string]any{}}, {"name": "workspace", "hostPath": map[string]any{"path": workspaceHostPath, "type": "DirectoryOrCreate"}},
}, },
}, },
} }
} }
func (a *App) remoteFlashPodSpec(name, host, image, node, device, artifactRef string) map[string]any { func (a *App) remoteFlashPodSpec(name, host, image, node, device, artifactRef string) map[string]any {
workspaceHostPath := remoteWorkspaceHostPath(a.settings.RemoteWorkspaceDir, name)
hostTmpPath := hostTmpHostPath(a.settings.HostTmpDir)
return map[string]any{ return map[string]any{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
@ -286,11 +330,11 @@ func (a *App) remoteFlashPodSpec(name, host, image, node, device, artifactRef st
}, },
"imagePullSecrets": []map[string]string{{"name": "harbor-regcred"}}, "imagePullSecrets": []map[string]string{{"name": "harbor-regcred"}},
"volumes": []map[string]any{ "volumes": []map[string]any{
{"name": "workspace", "emptyDir": map[string]any{}}, {"name": "workspace", "hostPath": map[string]any{"path": workspaceHostPath, "type": "DirectoryOrCreate"}},
{"name": "host-dev", "hostPath": map[string]any{"path": "/dev"}}, {"name": "host-dev", "hostPath": map[string]any{"path": "/dev"}},
{"name": "host-sys", "hostPath": map[string]any{"path": "/sys"}}, {"name": "host-sys", "hostPath": map[string]any{"path": "/sys"}},
{"name": "host-udev", "hostPath": map[string]any{"path": "/run/udev"}}, {"name": "host-udev", "hostPath": map[string]any{"path": "/run/udev"}},
{"name": "host-tmp", "hostPath": map[string]any{"path": "/tmp"}}, {"name": "host-tmp", "hostPath": map[string]any{"path": hostTmpPath, "type": "DirectoryOrCreate"}},
}, },
}, },
} }
@ -311,15 +355,7 @@ func inventoryNodeArch(spec *inventory.NodeSpec, class *inventory.NodeClass) str
} }
func mountedHostTmpDir(path string) string { func mountedHostTmpDir(path string) string {
path = strings.TrimSpace(path) return "/host-tmp"
switch {
case path == "", path == "/tmp":
return "/host-tmp"
case strings.HasPrefix(path, "/tmp/"):
return filepath.Join("/host-tmp", strings.TrimPrefix(path, "/tmp/"))
default:
return filepath.Join("/host-tmp", strings.TrimPrefix(path, "/"))
}
} }
func vaultRuntimeAnnotations(includeSSHKeys bool) map[string]string { func vaultRuntimeAnnotations(includeSSHKeys bool) map[string]string {

View File

@ -15,7 +15,7 @@ func TestRemoteHelperBranches(t *testing.T) {
if got := prettyDeviceTarget(""); got != "the selected target" { if got := prettyDeviceTarget(""); got != "the selected target" {
t.Fatalf("prettyDeviceTarget empty = %q", got) t.Fatalf("prettyDeviceTarget empty = %q", got)
} }
if got := prettyDeviceTarget("hosttmp:///tmp"); got != "/tmp" { if got := prettyDeviceTarget("hosttmp:///var/tmp/metis-flash-test"); got != "/var/tmp/metis-flash-test" {
t.Fatalf("prettyDeviceTarget hosttmp = %q", got) t.Fatalf("prettyDeviceTarget hosttmp = %q", got)
} }
if got := ramp(0, 10, 20, 1, 2); got != 1 { if got := ramp(0, 10, 20, 1, 2); got != 1 {
@ -27,10 +27,10 @@ func TestRemoteHelperBranches(t *testing.T) {
if got := ramp(20, 10, 20, 1, 2); got != 2 { if got := ramp(20, 10, 20, 1, 2); got != 2 {
t.Fatalf("ramp after end = %v", got) t.Fatalf("ramp after end = %v", got)
} }
if got := mountedHostTmpDir("/tmp/metis-flash-test"); got != "/host-tmp/metis-flash-test" { if got := mountedHostTmpDir("/tmp/metis-flash-test"); got != "/host-tmp" {
t.Fatalf("mountedHostTmpDir = %q", got) t.Fatalf("mountedHostTmpDir = %q", got)
} }
if got := mountedHostTmpDir("/var/tmp/metis-flash-test"); got != "/host-tmp/var/tmp/metis-flash-test" { if got := mountedHostTmpDir("/var/tmp/metis-flash-test"); got != "/host-tmp" {
t.Fatalf("mountedHostTmpDir non-tmp = %q", got) t.Fatalf("mountedHostTmpDir non-tmp = %q", got)
} }
if got := shellQuote(""); got != "''" { if got := shellQuote(""); got != "''" {
@ -247,6 +247,84 @@ func TestSelectBuilderHostScoresStorageAndAMD64(t *testing.T) {
} }
} }
func TestRemoteWorkspaceAndHostTmpPathsPreferUsbScratchRoots(t *testing.T) {
app := newTestApp(t)
app.settings.RemoteWorkspaceDir = "/var/tmp/metis-workspace"
app.settings.HostTmpDir = "/var/tmp/metis-flash-test"
buildSpec := app.remoteBuildPodSpec("metis-build-123", "titan-04", "runner:arm64", "titan-10", "registry.example/metis/titan-10", "build-1")
buildVolumes := buildSpec["spec"].(map[string]any)["volumes"].([]map[string]any)
workspaceVolume := buildVolumes[0]["hostPath"].(map[string]any)
if got := workspaceVolume["path"]; got != "/var/tmp/metis-workspace/metis-build-123" {
t.Fatalf("build workspace hostPath = %v", got)
}
flashSpec := app.remoteFlashPodSpec("metis-flash-123", "titan-04", "runner:arm64", "titan-10", hostTmpDevicePath, "registry.example/metis/titan-10")
flashVolumes := flashSpec["spec"].(map[string]any)["volumes"].([]map[string]any)
flashWorkspace := flashVolumes[0]["hostPath"].(map[string]any)
if got := flashWorkspace["path"]; got != "/var/tmp/metis-workspace/metis-flash-123" {
t.Fatalf("flash workspace hostPath = %v", got)
}
hostTmp := flashVolumes[4]["hostPath"].(map[string]any)
if got := hostTmp["path"]; got != "/var/tmp/metis-flash-test" {
t.Fatalf("host tmp hostPath = %v", got)
}
}
func TestSelectBuilderHostPrefersUsbScratchReadyWorkers(t *testing.T) {
kube := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/nodes":
_ = json.NewEncoder(w).Encode(map[string]any{
"items": []any{
map[string]any{
"metadata": map[string]any{
"name": "titan-04",
"labels": map[string]string{
"kubernetes.io/arch": "arm64",
"hardware": "rpi5",
"node-role.kubernetes.io/worker": "true",
},
"annotations": map[string]string{
"maintenance.bstein.dev/usb-scratch-status": "ok",
"maintenance.bstein.dev/usb-scratch-managed-paths": "/var/log/pods_/var/tmp",
},
},
"spec": map[string]any{"unschedulable": false},
},
map[string]any{
"metadata": map[string]any{
"name": "titan-05",
"labels": map[string]string{
"kubernetes.io/arch": "arm64",
"hardware": "rpi5",
"node-role.kubernetes.io/worker": "true",
},
},
"spec": map[string]any{"unschedulable": false},
},
},
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/namespaces/maintenance/pods":
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{}})
default:
http.NotFound(w, r)
}
}))
defer kube.Close()
installKubeFactory(t, kube)
app := newTestApp(t)
app.settings.Namespace = "maintenance"
node, err := app.selectBuilderHost("arm64", "")
if err != nil {
t.Fatalf("selectBuilderHost: %v", err)
}
if node.Name != "titan-04" {
t.Fatalf("expected titan-04 scratch-ready builder, got %s", node.Name)
}
}
func TestSelectBuilderHostTieBreaksByName(t *testing.T) { func TestSelectBuilderHostTieBreaksByName(t *testing.T) {
kube := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { kube := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch { switch {

View File

@ -6,8 +6,8 @@ import (
) )
func TestMountedHostTmpDirMapsConfiguredTmpPathIntoMount(t *testing.T) { func TestMountedHostTmpDirMapsConfiguredTmpPathIntoMount(t *testing.T) {
if got := mountedHostTmpDir("/tmp/metis-flash-test"); got != "/host-tmp/metis-flash-test" { if got := mountedHostTmpDir("/tmp/metis-flash-test"); got != "/host-tmp" {
t.Fatalf("expected /host-tmp/metis-flash-test, got %q", got) t.Fatalf("expected /host-tmp, got %q", got)
} }
if got := mountedHostTmpDir("/tmp"); got != "/host-tmp" { if got := mountedHostTmpDir("/tmp"); got != "/host-tmp" {
t.Fatalf("expected /host-tmp, got %q", got) t.Fatalf("expected /host-tmp, got %q", got)

View File

@ -11,29 +11,30 @@ var hostNameLookup = os.Hostname
// Settings configures the Metis service runtime. // Settings configures the Metis service runtime.
type Settings struct { type Settings struct {
BindAddr string BindAddr string
InventoryPath string InventoryPath string
CacheDir string CacheDir string
ArtifactDir string ArtifactDir string
ArtifactStatePath string ArtifactStatePath string
HistoryPath string HistoryPath string
SnapshotsPath string SnapshotsPath string
TargetsPath string TargetsPath string
DefaultFlashHost string DefaultFlashHost string
FlashHosts []string FlashHosts []string
LocalHost string LocalHost string
AllowedGroups []string AllowedGroups []string
MaxDeviceBytes int64 MaxDeviceBytes int64
Namespace string Namespace string
RunnerImageAMD64 string RunnerImageAMD64 string
RunnerImageARM64 string RunnerImageARM64 string
HarborRegistry string HarborRegistry string
HarborProject string HarborProject string
HarborAPIBase string HarborAPIBase string
HarborUsername string HarborUsername string
HarborPassword string HarborPassword string
HostTmpDir string HostTmpDir string
RemotePodTimeout int64 RemoteWorkspaceDir string
RemotePodTimeout int64
} }
// FromEnv builds service settings with sensible defaults for local dev and in-cluster use. // FromEnv builds service settings with sensible defaults for local dev and in-cluster use.
@ -43,29 +44,30 @@ func FromEnv() Settings {
defaultFlashHost := getenvDefault("METIS_DEFAULT_FLASH_HOST", localHost) defaultFlashHost := getenvDefault("METIS_DEFAULT_FLASH_HOST", localHost)
flashHosts := splitList(getenvDefault("METIS_FLASH_HOSTS", defaultFlashHost)) flashHosts := splitList(getenvDefault("METIS_FLASH_HOSTS", defaultFlashHost))
return Settings{ return Settings{
BindAddr: getenvDefault("METIS_BIND_ADDR", ":8080"), BindAddr: getenvDefault("METIS_BIND_ADDR", ":8080"),
InventoryPath: getenvDefault("METIS_INVENTORY_PATH", "inventory.titan-rpi4.yaml"), InventoryPath: getenvDefault("METIS_INVENTORY_PATH", "inventory.titan-rpi4.yaml"),
CacheDir: getenvDefault("METIS_CACHE_DIR", filepath.Join(dataDir, "cache")), CacheDir: getenvDefault("METIS_CACHE_DIR", filepath.Join(dataDir, "cache")),
ArtifactDir: getenvDefault("METIS_ARTIFACT_DIR", filepath.Join(dataDir, "artifacts")), ArtifactDir: getenvDefault("METIS_ARTIFACT_DIR", filepath.Join(dataDir, "artifacts")),
ArtifactStatePath: getenvDefault("METIS_ARTIFACT_STATE_PATH", filepath.Join(dataDir, "artifacts.json")), ArtifactStatePath: getenvDefault("METIS_ARTIFACT_STATE_PATH", filepath.Join(dataDir, "artifacts.json")),
HistoryPath: getenvDefault("METIS_HISTORY_PATH", filepath.Join(dataDir, "history.jsonl")), HistoryPath: getenvDefault("METIS_HISTORY_PATH", filepath.Join(dataDir, "history.jsonl")),
SnapshotsPath: getenvDefault("METIS_SNAPSHOTS_PATH", filepath.Join(dataDir, "snapshots.json")), SnapshotsPath: getenvDefault("METIS_SNAPSHOTS_PATH", filepath.Join(dataDir, "snapshots.json")),
TargetsPath: getenvDefault("METIS_TARGETS_PATH", filepath.Join(dataDir, "targets.json")), TargetsPath: getenvDefault("METIS_TARGETS_PATH", filepath.Join(dataDir, "targets.json")),
DefaultFlashHost: defaultFlashHost, DefaultFlashHost: defaultFlashHost,
FlashHosts: flashHosts, FlashHosts: flashHosts,
LocalHost: localHost, LocalHost: localHost,
AllowedGroups: splitList(getenvDefault("METIS_ALLOWED_GROUPS", "admin,maintenance")), AllowedGroups: splitList(getenvDefault("METIS_ALLOWED_GROUPS", "admin,maintenance")),
MaxDeviceBytes: getenvInt64("METIS_MAX_DEVICE_BYTES", 300000000000), MaxDeviceBytes: getenvInt64("METIS_MAX_DEVICE_BYTES", 300000000000),
Namespace: getenvDefault("METIS_NAMESPACE", "maintenance"), Namespace: getenvDefault("METIS_NAMESPACE", "maintenance"),
RunnerImageAMD64: getenvDefault("METIS_RUNNER_IMAGE_AMD64", ""), RunnerImageAMD64: getenvDefault("METIS_RUNNER_IMAGE_AMD64", ""),
RunnerImageARM64: getenvDefault("METIS_RUNNER_IMAGE_ARM64", ""), RunnerImageARM64: getenvDefault("METIS_RUNNER_IMAGE_ARM64", ""),
HarborRegistry: getenvDefault("METIS_HARBOR_REGISTRY", "registry.bstein.dev"), HarborRegistry: getenvDefault("METIS_HARBOR_REGISTRY", "registry.bstein.dev"),
HarborProject: getenvDefault("METIS_HARBOR_PROJECT", "metis"), HarborProject: getenvDefault("METIS_HARBOR_PROJECT", "metis"),
HarborAPIBase: getenvDefault("METIS_HARBOR_API_BASE", "https://registry.bstein.dev/api/v2.0"), HarborAPIBase: getenvDefault("METIS_HARBOR_API_BASE", "https://registry.bstein.dev/api/v2.0"),
HarborUsername: getenvDefault("METIS_HARBOR_USERNAME", ""), HarborUsername: getenvDefault("METIS_HARBOR_USERNAME", ""),
HarborPassword: getenvDefault("METIS_HARBOR_PASSWORD", ""), HarborPassword: getenvDefault("METIS_HARBOR_PASSWORD", ""),
HostTmpDir: getenvDefault("METIS_HOST_TMP_DIR", "/tmp/metis-flash-test"), HostTmpDir: getenvDefault("METIS_HOST_TMP_DIR", "/var/tmp/metis-flash-test"),
RemotePodTimeout: getenvInt64("METIS_REMOTE_POD_TIMEOUT_SEC", 1800), RemoteWorkspaceDir: getenvDefault("METIS_REMOTE_WORKSPACE_DIR", "/var/tmp/metis-workspace"),
RemotePodTimeout: getenvInt64("METIS_REMOTE_POD_TIMEOUT_SEC", 1800),
} }
} }

View File

@ -181,11 +181,11 @@ func fakeKubeServer(t *testing.T) *httptest.Server {
message := `{}` message := `{}`
switch { switch {
case strings.Contains(podName, "devices"): case strings.Contains(podName, "devices"):
message = `{"devices":[{"name":"sdz","path":"/dev/sdz","model":"Micro SD","transport":"usb","type":"disk","removable":true,"hotplug":true,"size_bytes":32000000000},{"name":"tmp","path":"hosttmp:///tmp","model":"Host /tmp","transport":"test","type":"file","note":"Test-only host write target under /tmp","size_bytes":1}]}` message = `{"devices":[{"name":"sdz","path":"/dev/sdz","model":"Micro SD","transport":"usb","type":"disk","removable":true,"hotplug":true,"size_bytes":32000000000},{"name":"tmp","path":"hosttmp:///var/tmp/metis-flash-test","model":"Host scratch","transport":"test","type":"file","note":"Test-only host write target under /var/tmp/metis-flash-test","size_bytes":1}]}`
case strings.Contains(podName, "build"): case strings.Contains(podName, "build"):
message = `{"local_path":"/workspace/build/titan-15.img.xz","compressed":true,"size_bytes":1234,"build_tag":"build-1"}` message = `{"local_path":"/workspace/build/titan-15.img.xz","compressed":true,"size_bytes":1234,"build_tag":"build-1"}`
case strings.Contains(podName, "flash"): case strings.Contains(podName, "flash"):
message = `{"dest_path":"/tmp/metis-flash-test/titan-15.img"}` message = `{"dest_path":"/var/tmp/metis-flash-test/titan-15.img"}`
} }
_ = json.NewEncoder(w).Encode(map[string]any{ _ = json.NewEncoder(w).Encode(map[string]any{
"metadata": map[string]any{"name": podName}, "metadata": map[string]any{"name": podName},