//go:build integration package main import ( "bytes" "encoding/json" "io" "os" "reflect" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/overlay" ) func TestAConvergedDeclarationIsByteForByteWhatItWas(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, helloWeb()) if _, err := unassign(ctx, open, "laptop", overlay.Name); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "laptop", "hello-web"); err != nil { t.Fatal(err) } declared := composed(t, open, "laptop") if declared.Adoption != nil { t.Fatal("a converged node was given an adoption envelope") } body, err := declared.Body() if err != nil { t.Fatal(err) } if string(body) != convergedBefore { t.Fatalf("a converged declaration changed; an older host parses this strictly:\n%s\n%s", body, convergedBefore) } if bytes.Contains(body, []byte(`"adoption"`)) { t.Fatal("a converged declaration names adoption; an older host would refuse it") } } func TestAnAdoptedDeclarationCarriesItsModeAndWhatWasTaken(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, helloWeb()) if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "anchor", "hello-web"); err != nil { t.Fatal(err) } declared := composed(t, open, "anchor") if declared.Adoption == nil { t.Fatal("an adopted node's declaration does not say it is adopted") } untaken := declared.Adoption.Untaken["hello-web"] // Every kind — its directory too (novox/hq ADR 0103). if !reflect.DeepEqual(untaken, []string{"hello-web.page", "hello-web.server", "hello-web.served"}) { t.Fatalf("hello-web's resources are not all named untaken: %v", declared.Adoption) } if len(declared.Adoption.Taken) != 0 { t.Fatalf("nothing was taken, and the declaration says %v", declared.Adoption.Taken) } body, err := declared.Body() if err != nil { t.Fatal(err) } var envelope map[string]any if err := json.Unmarshal(body, &envelope); err != nil { t.Fatal(err) } adoption, _ := envelope["adoption"].(map[string]any) if _, ok := adoption["taken"].([]any); !ok { t.Fatalf("taken is not a list on the wire, even empty: %s", body) } // The digest the mesh compares is the digest of what is sent: status and push agree. would, err := wouldSend(ctx, open, mustNodes(t, open)) if err != nil { t.Fatal(err) } if would["anchor"] != digestOf(body) { t.Fatal("the digest the mesh compares is not of the declaration push sends") } // plan --json prints that same envelope. printed := stdoutOf(t, func() error { return planCommand(ctx, []string{"anchor", "--json"}) }) var compact bytes.Buffer if err := json.Compact(&compact, []byte(printed)); err != nil { t.Fatalf("plan --json is not JSON: %v\n%s", err, printed) } if digestOf(compact.Bytes()) != digestOf(body) { t.Fatalf("plan --json shows something other than what push sends:\n%s", printed) } // Taking the module moves its resources out of untaken. if err := open.inventory.Take(ctx, "anchor", "hello-web"); err != nil { t.Fatal(err) } declared = composed(t, open, "anchor") if _, still := declared.Adoption.Untaken["hello-web"]; still { t.Fatalf("a taken module is still untaken: %v", declared.Adoption) } if !reflect.DeepEqual(declared.Adoption.Taken, []string{"hello-web"}) { t.Fatalf("taken is %v", declared.Adoption.Taken) } } func stdoutOf(t *testing.T, run func() error) string { t.Helper() r, w, err := os.Pipe() if err != nil { t.Fatal(err) } saved := os.Stdout os.Stdout = w done := make(chan string) go func() { all, _ := io.ReadAll(r) done <- string(all) }() runErr := run() os.Stdout = saved w.Close() out := <-done if runErr != nil { t.Fatalf("%v\n%s", runErr, out) } return out } func TestConsumersAreToldTheGivenPort(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, catalogue.Manifest{Module: "store", Version: "1", Provides: []catalogue.Offer{{Name: "database", Scope: catalogue.ScopeMesh}}, Listens: []catalogue.Listening{{Port: 5432, From: catalogue.FromMesh}}, Guards: []int{5432}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store", "ports": []any{"5432:5432"}, "image": "registry.example/pg@sha256:" + strings.Repeat("b", 64)}}}) register(t, open, catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"database"}}) if _, err := assign(ctx, open, "anchor", "store"); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "laptop", "app"); err != nil { t.Fatal(err) } if err := open.inventory.SetSettings(ctx, "anchor", "store", map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5433}}); err != nil { t.Fatal(err) } plan, _, err := planFor(ctx, open, "laptop") if err != nil { t.Fatal(err) } var told any for _, n := range plan.Needs { if n.Name == "database" { told = n.Serves["port"] } } if told != 5433 { t.Fatalf("the consumer is told the store is on %v", told) } for _, r := range composed(t, open, "anchor").Resources { if r["id"] == "store.server" && !reflect.DeepEqual(r["ports"], []any{"5433:5432"}) { t.Fatalf("the store publishes %v", r["ports"]) } } // A port for the whole mesh is refused where it is set, not stored to refuse every node's // declaration afterwards. if err := open.inventory.SetSettings(ctx, "", "store", map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5434}}); err == nil || !strings.Contains(err.Error(), "per node") { t.Fatalf("a port given for the whole mesh was not refused: %v", err) } plan, settings, err := planFor(ctx, open, "anchor") if err != nil { t.Fatal(err) } if _, err := declarationFor(ctx, open, "anchor", plan, settings); err != nil { t.Fatalf("the refused mesh-wide layer was stored anyway: %v", err) } } func TestTheGuardIsSentForTakenModulesOnly(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, catalogue.Manifest{Module: "store", Version: "1", Listens: []catalogue.Listening{{Port: 5432, From: catalogue.FromMesh}}, Guards: []int{5432}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store", "ports": []any{"5432:5432"}, "image": "registry.example/pg@sha256:" + strings.Repeat("b", 64)}}}) if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "anchor", "store"); err != nil { t.Fatal(err) } if hasID(composed(t, open, "anchor").Resources, catalogue.GuardID()) { t.Fatal("an untaken store is guarded") } if err := open.inventory.Take(ctx, "anchor", "store"); err != nil { t.Fatal(err) } for _, r := range composed(t, open, "anchor").Resources { if r["id"] == catalogue.GuardID() { if r["content"] != catalogue.AsGuard([]int{5432}) { t.Fatalf("the taken store's guard is:\n%s", r["content"]) } return } } t.Fatal("a taken store is not guarded") } func TestTheMachineSideOfAMappingIsMovedEverywhereTheNumberIsUsed(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, catalogue.Manifest{Module: "forge", Version: "1", Provides: []catalogue.Offer{{Name: "git-over-ssh", Scope: catalogue.ScopeMesh}}, Serves: map[string]map[string]any{"git-over-ssh": {"port": 2222}}, Listens: []catalogue.Listening{{Port: 2222, From: catalogue.FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "forge", "ports": []any{"2222:22"}, "image": "registry.example/forge@sha256:" + strings.Repeat("c", 64)}}}) register(t, open, catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"git-over-ssh"}}) if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "anchor", "forge"); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "laptop", "app"); err != nil { t.Fatal(err) } if err := open.inventory.Take(ctx, "anchor", "forge"); err != nil { t.Fatal(err) } if err := open.inventory.SetSettings(ctx, "anchor", "forge", map[string]any{catalogue.PortsSetting: map[string]any{"2222": 222}}); err != nil { t.Fatal(err) } resources := composed(t, open, "anchor").Resources var container, opening map[string]any for _, r := range resources { switch r["id"] { case "forge.server": container = r case catalogue.OpeningID("tcp", 222, catalogue.PathForwarded): opening = r } if id, _ := r["id"].(string); strings.HasPrefix(id, "adoption.opening-tcp-2222-") { t.Errorf("the adopted node is told to open the port the forge was moved off: %s", id) } } if container == nil || !reflect.DeepEqual(container["ports"], []any{"222:22"}) { t.Fatalf("the forge's container publishes %v", container["ports"]) } if opening == nil || opening["to"] != 22 || opening["from"] != catalogue.OpeningFromMesh { t.Fatalf("no opening for the port this node gave the forge: %v", opening) } var guard map[string]any for _, r := range resources { if r["id"] == catalogue.GuardID() { guard = r } } if guard == nil || guard["content"] != catalogue.AsGuard([]int{222}) { t.Fatalf("the guard does not refuse the port the forge is on:\n%v", guard["content"]) } // And the consumer on the other machine dials the same number. plan, _, err := planFor(ctx, open, "laptop") if err != nil { t.Fatal(err) } var told any for _, n := range plan.Needs { if n.Name == "git-over-ssh" { told = n.Serves["port"] } } if told != 222 { t.Fatalf("the consumer is told the forge answers on %v", told) } } func TestADefinitionMovingUnderAStoredSettingLeavesThatModuleOutNotTheMachine(t *testing.T) { open := aMesh(t) ctx := t.Context() web := helloWeb() web.Resources[1]["ports"] = []any{"8080"} register(t, open, web) register(t, open, catalogue.Manifest{Module: "notes", Version: "1", Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/notes.conf", "content": "x"}}}) for _, m := range []string{"hello-web", "notes"} { if _, err := assign(ctx, open, "laptop", m); err != nil { t.Fatal(err) } } // Judged where it is stored: a port the module does not publish is refused by name. err := open.inventory.SetSettings(ctx, "laptop", "hello-web", map[string]any{catalogue.PortsSetting: map[string]any{"9999": 10000}}) if err == nil || !strings.Contains(err.Error(), "hello-web on laptop") || !strings.Contains(err.Error(), "9999") { t.Fatalf("an impossible setting was stored: %v", err) } if err := open.inventory.SetSettings(ctx, "laptop", "hello-web", map[string]any{catalogue.PortsSetting: map[string]any{"8080": 10000}}); err != nil { t.Fatal(err) } if declared := composed(t, open, "laptop"); len(declared.LeftOut) != 0 { t.Fatalf("a setting that composes left a module out: %v", declared.LeftOut) } // The definition moves: the container publishes another port now. web.Version = "2" web.Resources[1]["ports"] = []any{"9090"} register(t, open, web) declared := composed(t, open, "laptop") if len(declared.LeftOut) != 1 || declared.LeftOut[0] != "hello-web" { t.Fatalf("hello-web is not left out: %v", declared.LeftOut) } if !strings.Contains(declared.leftOutWhy["hello-web"], "no container of its publishes 8080") { t.Fatalf("why it was left out is not said: %v", declared.leftOutWhy) } if hasID(declared.Resources, "hello-web.server") || !hasID(declared.Resources, "notes.conf") { t.Fatalf("the machine was not told everything else: %v", declared.Resources) } body, err := declared.Body() if err != nil { t.Fatal(err) } var env map[string]any if err := json.Unmarshal(body, &env); err != nil { t.Fatal(err) } left, _ := env["left_out"].([]any) if len(left) != 1 || left[0] != "hello-web" { t.Fatalf("the envelope does not say what was left out: %v", env) } }