package catalogue import ( "strings" "testing" ) // The case novox/hq issue 153 records: an adopted machine keeps its data where the predecessor put // it — a library on its own pool that must never move, a configuration on a second disk owned by // whoever the predecessor ran as. A definition may name none of that (ADR 0112); the assignment // says it, by the ids the definition declared, and the machine receives concrete paths as always. func placeable() Manifest { m := mod("arr", nil, nil, nil) m.Resources = []map[string]any{ {"id": "state", "type": "directory", "place": ".", "mode": "0700"}, {"id": "config", "type": "directory", "mode": "0755", "owner": "1000:1000"}, {"id": "server", "type": "container", "name": "arr", "image": "arr:1", "volumes": []any{"${dir:config}:/config", "${access:series}:/series", "${access:spool}:/downloads:ro"}, "env": map[string]any{"SPOOL": "${access:spool}"}}, } m.Accesses = []Access{{ID: "series", Mode: AccessReadWrite}, {ID: "spool"}} return m } func placedBy(values map[string]any) Rendering { return Rendering{Settings: SettingsBy{"arr": {{From: "node anchor", Values: values}}}} } func TestAnAssignmentPlacesDirectoriesAndAccesses(t *testing.T) { got, err := Resolve(shelf(placeable()), []string{"arr"}, workstation(), World{}) if err != nil { t.Fatal(err) } out, err := got.Declaration(placedBy(map[string]any{ PlacesSetting: map[string]any{ "config": map[string]any{"path": "/services/arr/config/", "owner": "1001:2000"}, }, AccessesSetting: map[string]any{ "series": "/storage/media/series", "spool": "/storage/downloads", }, })) if err != nil { t.Fatal(err) } seen := map[string]map[string]any{} for _, r := range out { seen[r["id"].(string)] = r } config := seen["arr.config"] if config["path"] != "/services/arr/config" || config["owner"] != "1001:2000" { t.Fatalf("the placed directory is %v %v; want the assignment's path and owner", config["path"], config["owner"]) } if seen["arr.state"]["path"] != "/var/lib/arr" { t.Fatalf("an unplaced directory left the default layout: %v", seen["arr.state"]["path"]) } var accesses []string for _, r := range out { if r["type"] == "access" { accesses = append(accesses, r["path"].(string)+" "+r["mode"].(string)) } } if strings.Join(accesses, ",") != "/storage/media/series read-write,/storage/downloads read" { t.Fatalf("the accesses reached the machine as %v", accesses) } server := seen["arr.server"] mounts := server["volumes"].([]any) if mounts[0] != "/services/arr/config:/config" || mounts[1] != "/storage/media/series:/series" || mounts[2] != "/storage/downloads:/downloads:ro" { t.Fatalf("the mounts were not filled with the placed paths: %v", mounts) } if server["env"].(map[string]any)["SPOOL"] != "/storage/downloads" { t.Fatalf("the environment was not filled: %v", server["env"]) } } // An access declared by id and placed by nobody resolves to nowhere, and that is refused with the // setting to write — not mounted as the literal, not skipped. func TestAnUnplacedAccessIsRefusedByName(t *testing.T) { got, err := Resolve(shelf(placeable()), []string{"arr"}, workstation(), World{}) if err != nil { t.Fatal(err) } _, err = got.Declaration(placedBy(map[string]any{ AccessesSetting: map[string]any{"series": "/storage/media/series"}, })) if err == nil || !strings.Contains(err.Error(), `"spool"`) || !strings.Contains(err.Error(), AccessesSetting) { t.Fatalf("an access nobody placed was not refused by name: %v", err) } } // Validated like endpoints: an id the module does not declare reaches nothing, and the refusal // says what it does declare; a relative path and a non-numeric owner are refused too. func TestPlacementsAreValidated(t *testing.T) { m := placeable() layers := func(values map[string]any) []Layer { return placedBy(values).Settings["arr"] } _, err := Places(m, layers(map[string]any{PlacesSetting: map[string]any{"data": "/mnt/data"}})) if err == nil || !strings.Contains(err.Error(), "does not declare") || !strings.Contains(err.Error(), `"config"`) { t.Fatalf("placing an undeclared directory was accepted: %v", err) } _, err = Places(m, layers(map[string]any{PlacesSetting: map[string]any{"config": "services/arr"}})) if err == nil || !strings.Contains(err.Error(), "absolute") { t.Fatalf("a relative placement was accepted: %v", err) } _, err = Places(m, layers(map[string]any{PlacesSetting: map[string]any{ "config": map[string]any{"path": "/services/arr", "owner": "media"}}})) if err == nil || !strings.Contains(err.Error(), "uid:gid") { t.Fatalf("a non-numeric owner was accepted: %v", err) } _, err = AccessPlaces(m, layers(map[string]any{AccessesSetting: map[string]any{"movies": "/storage/media/movies"}})) if err == nil || !strings.Contains(err.Error(), "does not declare") || !strings.Contains(err.Error(), `"series"`) { t.Fatalf("placing an undeclared access was accepted: %v", err) } _, err = AccessPlaces(m, layers(map[string]any{AccessesSetting: map[string]any{"series": "media/series"}})) if err == nil || !strings.Contains(err.Error(), "absolute") { t.Fatalf("a relative access was accepted: %v", err) } // And the two keys are never stray: they are validated here, not merged into a file. if stray := UnusedSettings(m, layers(map[string]any{ PlacesSetting: map[string]any{"config": "/services/arr/config"}, AccessesSetting: map[string]any{"series": "/storage/media/series"}, })); len(stray) != 0 { t.Fatalf("the placement keys were reported as unused: %v", stray) } } // A definition that carries a path still works, as the default the assignment may replace — and // the assignment's placement wins where both say. func TestADefinitionsPathIsTheDefaultTheAssignmentReplaces(t *testing.T) { m := placeable() m.Accesses = []Access{{ID: "series", Path: "/services/media/series", Mode: AccessReadWrite}, {ID: "spool", Path: "/services/media/downloads"}} got, err := Resolve(shelf(m), []string{"arr"}, workstation(), World{}) if err != nil { t.Fatal(err) } out, err := got.Declaration(placedBy(map[string]any{ PlacesSetting: map[string]any{"config": "/services/arr/config"}, AccessesSetting: map[string]any{"series": "/storage/media/series"}, })) if err != nil { t.Fatal(err) } var paths []string for _, r := range out { if r["type"] == "access" { paths = append(paths, r["path"].(string)) } } if strings.Join(paths, ",") != "/storage/media/series,/services/media/downloads" { t.Fatalf("placed one, defaulted the other: got %v", paths) } } // A reference to an access the definition does not declare is refused where the author is. func TestAnUnknownAccessReferenceIsRefusedAtParse(t *testing.T) { _, err := ParseManifest([]byte(`{ "module": "arr", "version": "1", "accesses": [{"id": "series", "mode": "read-write"}], "resources": [ {"id": "state", "type": "directory", "place": ".", "mode": "0700"}, {"id": "server", "type": "container", "name": "arr", "image": "arr:1", "volumes": ["${access:movies}:/movies"]} ]}`)) if err == nil || !strings.Contains(err.Error(), "${access:movies}") { t.Fatalf("a reference to an undeclared access was accepted: %v", err) } _, err = ParseManifest([]byte(`{"module": "arr", "version": "1", "accesses": [{"mode": "read"}]}`)) if err == nil || !strings.Contains(err.Error(), "neither an id nor a path") { t.Fatalf("an access with no id and no path was accepted: %v", err) } }