package release import ( "context" "errors" "testing" ) // Issue #315: a pass that cannot restart a named unit (the agent, User=carlos) // hands that route to root before it stages or repoints anything. The live // link keeps naming the running build, the version is not poisoned for the // other hosts in the pass, and root's pass then installs it normally. func TestRootRestartsUnitsHandsNamedUnitsToRoot(t *testing.T) { ctx := context.Background() s := NewDirStore(t.TempDir()) priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) t.Cleanup(func() { reg.Close() }) const consoleHost, appHost = "console.example.com", "shop.example.com" for _, ver := range []string{"v1", "v2"} { shipRelease(t, s, "bab", "myapp", ver, adoptPlatform, []byte(ver)) } if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, reg, consoleHost, "myapp", "canary/x", false) addRoute(t, reg, appHost, "myapp", "canary/x", false) if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { t.Fatal(err) } if err := reg.SetUnit(consoleHost, "console.service"); err != nil { t.Fatal(err) } if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil { t.Fatal(err) } var restarted []string restart := func(host string) error { restarted = append(restarted, host) if host == consoleHost { return errors.New("try-restart console.service: Interactive authentication required") } return nil } type req struct { host, ver string err error } var reqs []req relay := func(host, ver string, err error) { reqs = append(reqs, req{host, ver, err}) } if err := Pass(ctx, reg, s, pub, opt, restart, OnStageBlocked(relay), RootRestartsUnits()); err != nil { t.Fatal(err) } if len(reqs) != 1 || reqs[0].host != consoleHost || reqs[0].ver != "v2" || !errors.Is(reqs[0].err, ErrUnitRestartNeedsRoot) { t.Fatalf("relays = %+v, want one for %s v2 with ErrUnitRestartNeedsRoot", reqs, consoleHost) } if len(restarted) != 1 || restarted[0] != appHost { t.Fatalf("restarted = %v, want only %s", restarted, appHost) } if ver, err := LiveVersion(opt, consoleHost); err != nil || ver != "v1" { t.Fatalf("%s live = %q, %v; want v1 (untouched)", consoleHost, ver, err) } if rt, err := reg.Get(consoleHost); err != nil || rt == nil || rt.Version != "v1" { t.Fatalf("%s route = %+v, %v; want stamped v1", consoleHost, rt, err) } if ver, err := LiveVersion(opt, appHost); err != nil || ver != "v2" { t.Fatalf("%s live = %q, %v; want v2 (not poisoned by the hand-off)", appHost, ver, err) } if r, err := ReadRefusal(opt, consoleHost); err != nil || r != nil { t.Fatalf("refusal for %s = %+v, %v; want none", consoleHost, r, err) } // Root's pass, without the option, installs it. restarted = nil rootRestart := func(host string) error { restarted = append(restarted, host); return nil } if err := Pass(ctx, reg, s, pub, opt, rootRestart); err != nil { t.Fatal(err) } if ver, err := LiveVersion(opt, consoleHost); err != nil || ver != "v2" || len(restarted) != 1 || restarted[0] != consoleHost { t.Fatalf("root pass: %s live = %q, %v, restarted %v; want v2 and one restart", consoleHost, ver, err, restarted) } } // The option hands nothing over for a route this pass would not restart, and // leaves routes with no unit to the pass. func TestRootRestartsUnitsNeedsANamedUnitAndARestart(t *testing.T) { ctx := context.Background() s := NewDirStore(t.TempDir()) priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) t.Cleanup(func() { reg.Close() }) const host = "console.example.com" shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("v1")) if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, reg, host, "myapp", "canary/x", false) if err := reg.SetUnit(host, "console.service"); err != nil { t.Fatal(err) } relays := 0 relay := func(string, string, error) { relays++ } // restart nil: a pure repoint, which needs no unit restart. if err := Pass(ctx, reg, s, pub, opt, nil, OnStageBlocked(relay), RootRestartsUnits()); err != nil { t.Fatal(err) } if ver, err := LiveVersion(opt, host); err != nil || ver != "v1" || relays != 0 { t.Fatalf("live = %q, %v, relays %d; want v1 and no relay", ver, err, relays) } } // Astra review of #315: a route with no unit of its own whose process is a // named unit (same account, app, channel and upstream) is root's too. Stamped // by the agent it would claim a build console.service does not run. func TestRootRestartsUnitsHandsTheProcessGroupToRoot(t *testing.T) { ctx, reg, s, pub, opt := startUnitFixture(t, "console.example.com") if err := reg.SetUnit("console.example.com", ""); err != nil { t.Fatal(err) } groupFixtureRoute(t, reg, "platform.example.com", "", "127.0.0.1:8090", "console.service") if err := reg.SetUpstreams("console.example.com", "127.0.0.1:8090", "", ""); err != nil { t.Fatal(err) } if err := PointLive(opt, "platform.example.com", "bab", "old", "console"); err != nil { t.Fatal(err) } var relayed []string relay := func(host, ver string, err error) { if errors.Is(err, ErrUnitRestartNeedsRoot) { relayed = append(relayed, host) } } restart := func(host string) error { t.Errorf("restart(%s): every host here is the unit's", host) return nil } if err := Pass(ctx, reg, s, pub, opt, restart, OnStageBlocked(relay), RootRestartsUnits()); err != nil { t.Fatal(err) } if len(relayed) != 2 { t.Fatalf("relayed = %v, want both hosts", relayed) } for _, h := range []string{"console.example.com", "platform.example.com"} { if ver, err := LiveVersion(opt, h); err != nil || ver != "old" { t.Fatalf("%s live = %q, %v; want old", h, ver, err) } } }