// SPDX-License-Identifier: AGPL-3.0-only package deploy import ( "context" "os" "path/filepath" "strings" "testing" "time" "gamertan.com/tend/internal/config" "gamertan.com/tend/internal/state" ) func writeReleaseIdentity(t *testing.T, release, version, commit string) { t.Helper() body := `{"version":"` + version + `","commit":"` + commit + `"}` if err := os.WriteFile(filepath.Join(release, "RELEASE.json"), []byte(body), 0o644); err != nil { t.Fatal(err) } } func storeSingletonState(t *testing.T, cfg config.Config, active, previous string) { t.Helper() record := state.Record{ SchemaVersion: state.SchemaVersion, Strategy: "singleton_candidate", DesiredRelease: active, ActiveSlot: "singleton", ActiveRelease: active, PreviousRelease: previous, LastAttemptRelease: active, LastAttemptOutcome: "succeeded", LastAttemptAt: time.Unix(100, 0).UTC().Format(time.RFC3339), UpdatedAt: time.Unix(100, 0).UTC().Format(time.RFC3339), } if err := state.Store(cfg.Deployment.StateFile, cfg.Deployment.Root, record); err != nil { t.Fatal(err) } } func reconciliationManager(operator Operator) Manager { return Manager{Operator: operator, ReadIdentity: readReleaseIdentity} } func TestReconcileReportsHealthySingletonWithoutMutation(t *testing.T) { cfg, active, previous := baseConfig(t, "singleton_candidate") cfg.Deployment.Singleton, _ = singletonSettings(t, cfg, active) if err := replaceSymlink(cfg.Deployment.Singleton.PreviousLink, previous); err != nil { t.Fatal(err) } commit := strings.Repeat("a", 40) writeReleaseIdentity(t, active, "v0.2.0-preview.2", commit) storeSingletonState(t, cfg, active, previous) operator := &fakeOperator{active: map[string]bool{ cfg.Deployment.Singleton.Unit: true, cfg.Service.Name + "-tend-candidate.service": false, }} report, err := reconciliationManager(operator).Reconcile(context.Background(), cfg) if err != nil { t.Fatal(err) } if !report.Consistent || report.Mutation != "none" || !report.StateInitialized || len(report.Findings) != 0 { t.Fatalf("report=%+v", report) } if report.Observed.ActiveRelease != active || report.Observed.PreviousRelease != previous || !report.Observed.RouteHandlerMatches { t.Fatalf("observed=%+v", report.Observed) } if report.Observed.ActiveIdentity == nil || report.Observed.ActiveIdentity.Version != "v0.2.0-preview.2" || report.Observed.ActiveIdentity.Commit != commit { t.Fatalf("identity=%+v", report.Observed.ActiveIdentity) } if !report.Observed.Units[cfg.Deployment.Singleton.Unit] || report.Observed.CandidateUnitActive == nil || *report.Observed.CandidateUnitActive { t.Fatalf("units=%#v candidate=%v", report.Observed.Units, report.Observed.CandidateUnitActive) } } func TestReconcileReportsHealthyBlueGreenDeployment(t *testing.T) { cfg, active, previous := baseConfig(t, "blue_green") handler := filepath.Join(cfg.Deployment.Root, "handler.caddy") template := filepath.Join(cfg.Deployment.Root, "handler.template") if err := os.WriteFile(handler, []byte("reverse_proxy 127.0.0.1:8090\n"), 0o640); err != nil { t.Fatal(err) } if err := os.WriteFile(template, []byte("reverse_proxy {{UPSTREAM}}\n"), 0o644); err != nil { t.Fatal(err) } blue := filepath.Join(cfg.Deployment.Root, "slots", "blue") green := filepath.Join(cfg.Deployment.Root, "slots", "green") if err := replaceSymlink(blue, active); err != nil { t.Fatal(err) } if err := replaceSymlink(green, previous); err != nil { t.Fatal(err) } cfg.Deployment.BlueGreen = &config.BlueGreen{ CaddyConfig: filepath.Join(cfg.Deployment.Root, "Caddyfile"), CaddyHandler: handler, CaddyHandlerTemplate: template, BootstrapActive: "blue", Blue: config.Slot{Unit: "example-blue.service", Address: "127.0.0.1:8090", Link: blue}, Green: config.Slot{Unit: "example-green.service", Address: "127.0.0.1:8091", Link: green}, } writeReleaseIdentity(t, active, "v0.2.0-preview.2", strings.Repeat("c", 40)) record := state.Record{ SchemaVersion: state.SchemaVersion, Strategy: "blue_green", DesiredRelease: active, ActiveSlot: "blue", ActiveRelease: active, PreviousSlot: "green", PreviousRelease: previous, LastAttemptRelease: active, LastAttemptOutcome: "succeeded", LastAttemptAt: time.Unix(100, 0).UTC().Format(time.RFC3339), UpdatedAt: time.Unix(100, 0).UTC().Format(time.RFC3339), } if err := state.Store(cfg.Deployment.StateFile, cfg.Deployment.Root, record); err != nil { t.Fatal(err) } operator := &fakeOperator{active: map[string]bool{ "example-blue.service": true, "example-green.service": true, }} report, err := reconciliationManager(operator).Reconcile(context.Background(), cfg) if err != nil { t.Fatal(err) } if !report.Consistent || report.Mutation != "none" || report.Observed.ActiveRelease != active || report.Observed.PreviousRelease != previous || !report.Observed.RouteHandlerMatches { t.Fatalf("report=%+v", report) } if report.Observed.CandidateUnit != "" || report.Observed.CandidateUnitActive != nil { t.Fatalf("unexpected candidate observation=%+v", report.Observed) } } func TestReconcileExplainsDriftWithoutRepairingIt(t *testing.T) { cfg, recorded, observed := baseConfig(t, "singleton_candidate") var handler string cfg.Deployment.Singleton, handler = singletonSettings(t, cfg, recorded) if err := replaceSymlink(cfg.Deployment.Singleton.PreviousLink, observed); err != nil { t.Fatal(err) } storeSingletonState(t, cfg, recorded, observed) if err := replaceSymlink(cfg.Deployment.Singleton.CurrentLink, observed); err != nil { t.Fatal(err) } writeReleaseIdentity(t, observed, "v0.2.0-preview.3", strings.Repeat("b", 40)) candidateHandler, err := renderHandler(cfg.Deployment.Singleton.CaddyHandlerTemplate, cfg.Deployment.Singleton.CandidateAddress) if err != nil { t.Fatal(err) } if err := os.WriteFile(handler, candidateHandler, 0o640); err != nil { t.Fatal(err) } beforeHandler, err := os.ReadFile(handler) if err != nil { t.Fatal(err) } operator := &fakeOperator{active: map[string]bool{ cfg.Deployment.Singleton.Unit: false, cfg.Service.Name + "-tend-candidate.service": true, }} report, err := reconciliationManager(operator).Reconcile(context.Background(), cfg) if err != nil { t.Fatal(err) } if report.Consistent || report.Mutation != "none" { t.Fatalf("report=%+v", report) } codes := map[string]bool{} for _, finding := range report.Findings { codes[finding.Code] = true } for _, code := range []string{"active_release_drift", "active_unit_inactive", "route_handler_drift", "unleased_candidate_active"} { if !codes[code] { t.Fatalf("missing %s in %#v", code, report.Findings) } } target, err := resolveReleaseLink(cfg.Deployment.Root, cfg.Deployment.Singleton.CurrentLink) if err != nil || target != observed { t.Fatalf("current=%q err=%v", target, err) } afterHandler, err := os.ReadFile(handler) if err != nil || string(afterHandler) != string(beforeHandler) { t.Fatalf("handler changed err=%v", err) } }