Export the reviewed allowlisted snapshot from private source commit 07c1655921f21ee5e4fc4d85639d199e8867b17d. This records the Docker Compose activation, schema-compatible rollback, and stateful migration resource findings from Observatory Preview 19 dogfooding. AI-Assisted: OpenAI Codex Signed-off-by: Cole Speelman <crspeelman@gmail.com>
183 lines
6.9 KiB
Go
183 lines
6.9 KiB
Go
// 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)
|
|
}
|
|
}
|