package apply import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/store" ) // The resolver's configuration named a second file, the mesh's zones, declared after the service — // as a module's facts are, after its resources. The host restarted the service for the // configuration, the daemon could not read the zones file the same apply had not written yet, and // the apply failed on every machine (novox/hq issue 260). A service is restarted only once every // resource it names under restart-on has been applied. func TestAServiceIsRestartedOnlyOnceEveryFileItNamesIsWritten(t *testing.T) { dir := t.TempDir() conf := filepath.Join(dir, "resolver.conf") zones := filepath.Join(dir, "zones.conf") declare := func(zonesContent string) string { return fmt.Sprintf(`{"declaration":1,"resources":[ {"id":"resolver.config","type":"file","path":%q,"content":"conf-file=%s\n","mode":"0644"}, {"id":"resolver.service","type":"service","unit":"resolver.service","state":"running", "restart-on":["resolver.config","resolver.fact-zones"]}, {"id":"resolver.fact-zones","type":"file","path":%q,"content":%q,"mode":"0644"} ]}`, conf, zones, zones, zonesContent) } // A daemon that cannot start without both of its files, as dnsmasq cannot. var commands []string services := recordingServices(&commands) run := func(ctx context.Context, name string, args ...string) (string, error) { if strings.Contains(strings.Join(args, " "), "start resolver.service") { if _, err := os.Stat(zones); err != nil { commands = append(commands, name+" "+strings.Join(args, " ")) return "", errors.New("cannot read " + zones + ": no such file or directory") } } return services(ctx, name, args...) } report, state, err := Apply(context.Background(), archHost(t), parse(t, declare("server=/a/1\n")), store.State{}, store.OriginCarried, run, nil, nil) if err != nil { t.Fatalf("the service was restarted before every file it reads was written: %v", err) } var detail string for _, o := range report.Outcomes { if o.ID == "resolver.service" { detail = o.Detail } } if !strings.Contains(detail, "resolver.config") || !strings.Contains(detail, "resolver.fact-zones") { t.Errorf("one restart for both files was expected, the outcome says %q", detail) } restarts := 0 for _, c := range commands { if strings.Contains(c, "stop resolver.service") { restarts++ } } if restarts != 1 { t.Errorf("the service was restarted %d times; once, after both files: %v", restarts, commands) } // And a change to the file declared after the service alone is acted on in the apply that // makes it — not passed by because the service was reached first. commands = nil if _, _, err := Apply(context.Background(), archHost(t), parse(t, declare("server=/b/2\n")), state, store.OriginCarried, run, nil, nil); err != nil { t.Fatal(err) } restarted := false for _, c := range commands { if strings.Contains(c, "stop resolver.service") { restarted = true } } if !restarted { t.Errorf("the zones file changed and the service was not restarted: %v", commands) } } // Only a service moves, and only to just after the last resource it names — under restart-on or // reload-on, in its own module or another's. Everything else keeps its declared place, and a name // not in the declaration is no reason to move. func TestAServiceIsOrderedAfterWhatItNamesAndNothingElseMoves(t *testing.T) { d := parse(t, `{"declaration":1,"resources":[ {"id":"a.dir","type":"directory","path":"/tmp/a"}, {"id":"a.runtime","type":"service","unit":"docker.service","state":"running","reload-on":["b.daemon"]}, {"id":"a.svc","type":"service","unit":"a.service","state":"running","restart-on":["a.conf","gone"]}, {"id":"a.conf","type":"file","path":"/tmp/a/conf","content":"x\n"}, {"id":"b.daemon","type":"file","path":"/tmp/b/daemon.json","content":"{}\n"}, {"id":"b.after","type":"file","path":"/tmp/b/after","content":"y\n"}, {"id":"b.svc","type":"service","unit":"b.service","state":"running","restart-on":["a.conf"]} ]}`) var got []string for _, r := range afterWhatTheyRead(d.Resources) { got = append(got, r.Identity()) } want := "a.dir a.conf a.svc b.daemon a.runtime b.after b.svc" if strings.Join(got, " ") != want { t.Errorf("ordered\n %s\nwant\n %s", strings.Join(got, " "), want) } }