Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions internal/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package catalog

import (
"errors"
"fmt"
"io/fs"
"log"
"math"
"net/url"
"os"
"path"
Expand Down Expand Up @@ -276,6 +278,13 @@ type DockerConfig struct {
// container health cannot see. For services with no collector it is the only
// signal there is.
HealthSignals []HealthSignal `json:"healthSignals" yaml:"health_signals"`
// HealthProbe is the web catalog's docker.health_probe block: a read-only command
// the web worker runs inside a running node, with regex checks on its answer
// (Mysterium's monitoring status and recent sessions). Neither schema file
// describes it yet. Desktop parses it so the field round-trips and the
// strict-decode test passes, and does not act on it. The loader makes it
// JSON-safe (see jsonSafe), since the service list crosses the Wails bridge.
HealthProbe map[string]any `json:"healthProbe,omitempty" yaml:"health_probe"`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// AdvertisedAddressEnv names the ONE env var holding the address the network
// dials this service back at, so it can be compared against the machine's
// current egress address. Only that variable is ever copied out; the rest may
Expand Down Expand Up @@ -516,6 +525,39 @@ func normalize(svc *Service) {
svc.SupportURL = ""
}
svc.Docker.HealthSignals = normalizeSignals(svc.Slug, svc.Docker.HealthSignals)
if svc.Docker.HealthProbe != nil {
svc.Docker.HealthProbe = jsonSafe(svc.Docker.HealthProbe).(map[string]any)
}
}

// jsonSafe turns the map[any]any that yaml.v3 produces for a nested mapping with a
// non-string key (an unquoted number, say) into map[string]any, and a .nan, .inf or
// -.inf into its string form ("NaN", "+Inf", "-Inf"). encoding/json refuses both,
// and one such value would fail the whole service list on its way to the UI.
func jsonSafe(v any) any {
switch t := v.(type) {
case map[string]any:
for k, e := range t {
t[k] = jsonSafe(e)
}
return t
case map[any]any:
m := make(map[string]any, len(t))
for k, e := range t {
m[fmt.Sprint(k)] = jsonSafe(e)
}
return m
case []any:
for i, e := range t {
t[i] = jsonSafe(e)
}
return t
case float64:
if math.IsNaN(t) || math.IsInf(t, 0) {
return fmt.Sprint(t)
}
}
return v
}

// normalizeSignals lowercases each state and drops the signals the web catalog
Expand Down
73 changes: 73 additions & 0 deletions internal/catalog/health_probe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package catalog

import (
"encoding/json"
"testing"
"testing/fstest"
)

// TestHealthProbeMarshalsToJSON. yaml.v3 decodes a nested mapping with an unquoted
// numeric key into map[any]any, which encoding/json refuses; the service list then
// cannot reach the UI and the app shows nothing.
func TestHealthProbeMarshalsToJSON(t *testing.T) {
fsys := fstest.MapFS{"services/x/a.yml": {Data: []byte(`
name: A
slug: a
docker:
image: a:1
health_probe:
command: [myst, status]
checks:
- expect:
200: ok
state: {1: up}
`)}}
cat, err := LoadEmbedded(fsys)
if err != nil {
t.Fatal(err)
}
svc := mustGet(t, cat, "a")
out, err := json.Marshal(svc)
if err != nil {
t.Fatalf("service with a numeric-keyed health_probe must marshal to JSON: %v", err)
}
var back struct {
Docker struct {
HealthProbe map[string]any `json:"healthProbe"`
} `json:"docker"`
}
if err := json.Unmarshal(out, &back); err != nil {
t.Fatal(err)
}
expect := back.Docker.HealthProbe["checks"].([]any)[0].(map[string]any)["expect"].(map[string]any)
if expect["200"] != "ok" || expect["state"].(map[string]any)["1"] != "up" {
t.Errorf("probe keys lost on the way through: %v", expect)
}
}

// TestHealthProbeNonFiniteFloatMarshals. YAML .nan, .inf and -.inf decode to
// float64 NaN and Inf, which encoding/json refuses just like map[any]any.
func TestHealthProbeNonFiniteFloatMarshals(t *testing.T) {
fsys := fstest.MapFS{"services/x/a.yml": {Data: []byte(`
name: A
slug: a
docker:
image: a:1
health_probe:
threshold: .nan
checks:
- {max: .inf, min: -.inf, ok: 1.5}
`)}}
cat, err := LoadEmbedded(fsys)
if err != nil {
t.Fatal(err)
}
if _, err := json.Marshal(cat.ListVisible()); err != nil {
t.Fatalf("service list with a non-finite health_probe float must marshal to JSON: %v", err)
}
p := mustGet(t, cat, "a").Docker.HealthProbe
c := p["checks"].([]any)[0].(map[string]any)
if p["threshold"] != "NaN" || c["max"] != "+Inf" || c["min"] != "-Inf" || c["ok"] != 1.5 {
t.Errorf("probe floats not normalised as expected: %v", p)
}
}
Loading