Files
yms-daemon/internal/backendupdate/updater_test.go
T

332 lines
11 KiB
Go

package backendupdate
import (
"archive/zip"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log/slog"
"os"
"path/filepath"
"testing"
"yms-daemon/internal/deploymentconfig"
"yms-daemon/internal/hostnginx"
"yms-daemon/internal/nativebackendexecutor"
"yms-daemon/internal/systemd"
"yms-daemon/internal/transaction"
"yms-daemon/internal/updatepackage"
)
func TestUpdaterMigratesLegacyBackendAndCommits(t *testing.T) {
testUpdaterMigratesLegacyBackendAndCommits(t, false)
}
func TestUpdaterCommitsDirectNativeJAR(t *testing.T) {
testUpdaterMigratesLegacyBackendAndCommits(t, true)
}
func testUpdaterMigratesLegacyBackendAndCommits(t *testing.T, direct bool) {
t.Helper()
ctx := context.Background()
root := t.TempDir()
store, err := transaction.OpenStore(ctx, filepath.Join(root, "transactions.db"))
if err != nil {
t.Fatalf("open transaction store: %v", err)
}
defer store.Close()
coordinator, err := transaction.NewCoordinator(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
if err != nil {
t.Fatalf("create transaction coordinator: %v", err)
}
releaseDir := filepath.Join(root, "releases")
activeJAR := filepath.Join(root, "lib", "glory-soft-yms.jar")
if err := os.MkdirAll(filepath.Dir(activeJAR), 0o750); err != nil {
t.Fatalf("create backend lib directory: %v", err)
}
if err := os.WriteFile(activeJAR, []byte("legacy backend JAR"), 0o644); err != nil {
t.Fatalf("write legacy backend JAR: %v", err)
}
config := deploymentconfig.Config{Backend: deploymentconfig.Backend{
Type: deploymentconfig.BackendTypeNative,
ReleaseDir: releaseDir,
ActiveJAR: activeJAR,
Slot: deploymentconfig.BackendSlots{
Port8080: deploymentconfig.BackendSlot{
Unit: "yms-backend@8080.service",
JAR: filepath.Join(root, "lib", "glory-soft-yms-8080.jar"),
HealthEndpoint: "http://127.0.0.1:8080/yms/actuator/health",
},
Port8081: deploymentconfig.BackendSlot{
Unit: "yms-backend@8081.service",
JAR: filepath.Join(root, "lib", "glory-soft-yms-8081.jar"),
HealthEndpoint: "http://127.0.0.1:8081/yms/actuator/health",
},
},
}}
units := &updateUnitManager{units: map[string]systemd.Unit{
"yms-backend@8080.service": {Name: "yms-backend@8080.service", LoadState: "loaded", ActiveState: "inactive"},
"yms-backend@8081.service": {Name: "yms-backend@8081.service", LoadState: "loaded", ActiveState: "inactive"},
legacyUnit8080: {Name: legacyUnit8080, LoadState: "loaded", ActiveState: "failed"},
legacyUnit8081: {Name: legacyUnit8081, LoadState: "loaded", ActiveState: "activating"},
}}
gateway := &memoryGateway{snapshot: hostnginx.Snapshot{Content: []byte(serverConfiguration8081), ActivePort: 8081}}
executor := &preparingExecutor{store: store, releaseDir: releaseDir, units: units}
updater := &Updater{
config: config,
workRoot: filepath.Join(root, "work"),
store: store,
coordinator: coordinator,
units: units,
gateway: gateway,
executor: executor,
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
drain: 0,
}
var record transaction.Transaction
var installedPath string
var progress []Progress
report := func(event Progress) {
progress = append(progress, event)
}
if direct {
jarPath := writeDirectBackendJAR(t, []byte("new backend JAR"))
jar, err := updatepackage.OpenDirectNativeJAR(jarPath)
if err != nil {
t.Fatalf("inspect direct native backend JAR: %v", err)
}
installedPath = filepath.Join(releaseDir, "direct", jar.SHA256[:12], jar.FileName)
record, err = updater.UpdateNativeJAR(ctx, jarPath, report)
} else {
packagePath := writeNativeBackendPackage(t, []byte("new backend JAR"))
installedPath = filepath.Join(releaseDir, "glory-soft-yms-test.jar")
record, err = updater.UpdateRepack(ctx, packagePath, report)
}
if err != nil {
t.Fatalf("update native backend: %v", err)
}
if record.State != transaction.StateCommitted || gateway.snapshot.ActivePort != 8080 {
t.Fatalf("unexpected committed update: record=%+v gateway=%+v", record, gateway.snapshot)
}
if target, err := os.Readlink(activeJAR); err != nil || target != installedPath {
t.Fatalf("unexpected compatibility link: target=%q installed=%q err=%v", target, installedPath, err)
}
if units.units[legacyUnit8081].ActiveState != "failed" || units.stopped != legacyUnit8081 {
t.Fatalf("legacy backend unit was not stopped: unit=%+v stopped=%s", units.units[legacyUnit8081], units.stopped)
}
if len(progress) == 0 || progress[len(progress)-1].State != transaction.StateCommitted {
t.Fatalf("missing committed update progress: %+v", progress)
}
if direct {
previousTransactionID := record.ID
restartSource, sourceErr := currentReleaseSource(activeJAR)
if sourceErr != nil {
t.Fatalf("resolve restart source: %v", sourceErr)
}
restartJAR, sourceErr := updatepackage.OpenDirectNativeJAR(restartSource)
if sourceErr != nil {
t.Fatalf("open restart source: %v", sourceErr)
}
restartReleasePath, sourceErr := updater.restartReleasePath(restartJAR)
if sourceErr != nil {
t.Fatalf("resolve restart release path: %v", sourceErr)
}
pendingRestart, _, created, sourceErr := updater.createOrResume(ctx, updateInput{
IdempotencyKey: "backend:restart:test-resume",
InputType: inputTypeCurrentRelease,
SourcePath: restartJAR.Path,
SourceSHA256: restartJAR.SHA256,
ArtifactFileName: restartJAR.FileName,
ArtifactIdentity: restartJAR.Identity,
ReleasePath: restartReleasePath,
Materialize: restartJAR.CopyArtifact,
})
if sourceErr != nil || !created {
t.Fatalf("create interrupted restart transaction: record=%+v created=%t err=%v", pendingRestart, created, sourceErr)
}
progress = nil
record, err = updater.Restart(ctx, report)
if err != nil {
t.Fatalf("restart native backend: %v", err)
}
if record.ID == previousTransactionID || record.ID != pendingRestart.ID || record.State != transaction.StateCommitted {
t.Fatalf("unexpected resumed restart transaction: previous=%s pending=%s record=%+v", previousTransactionID, pendingRestart.ID, record)
}
if gateway.snapshot.ActivePort != 8081 || units.units["yms-backend@8081.service"].ActiveState != "active" || units.units["yms-backend@8080.service"].ActiveState != "failed" {
t.Fatalf("restart did not rotate native backend slots: gateway=%+v units=%+v", gateway.snapshot, units.units)
}
if len(progress) == 0 || progress[len(progress)-1].Message != "Native backend restart committed" {
t.Fatalf("missing committed restart progress: %+v", progress)
}
}
}
const serverConfiguration8081 = `http {
upstream yms-server {
# yms-update managed upstream begin
# server 10.11.1.117:8080 max_fails=1 fail_timeout=2s;
server 10.11.1.117:8081 max_fails=1 fail_timeout=2s;
# yms-update managed upstream end
}
}
`
type memoryGateway struct {
snapshot hostnginx.Snapshot
}
func (g *memoryGateway) Read() (hostnginx.Snapshot, error) {
return hostnginx.Snapshot{Content: append([]byte(nil), g.snapshot.Content...), ActivePort: g.snapshot.ActivePort}, nil
}
func (g *memoryGateway) Apply(_ context.Context, snapshot hostnginx.Snapshot) error {
g.snapshot = hostnginx.Snapshot{Content: append([]byte(nil), snapshot.Content...), ActivePort: snapshot.ActivePort}
return nil
}
type updateUnitManager struct {
units map[string]systemd.Unit
stopped string
}
func (m *updateUnitManager) Inspect(_ context.Context, name string) (systemd.Unit, error) {
return m.units[name], nil
}
func (m *updateUnitManager) Start(_ context.Context, name string) error {
unit := m.units[name]
unit.ActiveState = "active"
m.units[name] = unit
return nil
}
func (m *updateUnitManager) Stop(_ context.Context, name string) error {
unit := m.units[name]
unit.ActiveState = "failed"
m.units[name] = unit
m.stopped = name
return nil
}
type preparingExecutor struct {
store *transaction.Store
releaseDir string
units *updateUnitManager
}
func (e *preparingExecutor) Run(ctx context.Context, transactionID string, request nativebackendexecutor.Request) error {
if err := os.MkdirAll(e.releaseDir, 0o750); err != nil {
return err
}
content, err := os.ReadFile(request.ArtifactPath)
if err != nil {
return err
}
installedPath := filepath.Join(e.releaseDir, request.ReleasePath)
if err := os.MkdirAll(filepath.Dir(installedPath), 0o750); err != nil {
return err
}
if err := os.WriteFile(installedPath, content, 0o640); err != nil {
return err
}
if err := os.Symlink(installedPath, request.SlotJarPath); err != nil {
return err
}
if err := e.units.Start(ctx, request.UnitName); err != nil {
return err
}
for _, state := range []transaction.State{
transaction.StateValidating,
transaction.StatePrepared,
transaction.StateStarting,
transaction.StateSwitching,
} {
if _, err := e.store.Transition(ctx, transactionID, state, "test transition"); err != nil {
return err
}
}
return nil
}
func writeNativeBackendPackage(t *testing.T, jar []byte) string {
t.Helper()
digest := sha256.Sum256(jar)
manifest := map[string]any{
"customerCode": "customer-01",
"customerDisplayName": "Customer 01",
"versionId": "V1.1.8",
"items": []string{"deploy-sync.sh"},
"backendArtifacts": []any{map[string]any{
"id": int64(42),
"versionCode": "V1.1.8",
"artifactKind": "BACKEND",
"type": "native",
"selectedType": "native",
"platform": nil,
"fileName": "glory-soft-yms-test.jar",
"filePath": "/archive/glory-soft-yms-test.jar",
"sha256": hex.EncodeToString(digest[:]),
"imageRef": nil,
}},
"frontendArtifacts": []any{},
"nodeSsrArtifacts": []any{},
"remark": nil,
}
manifestContent, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("encode artifact selection: %v", err)
}
packagePath := filepath.Join(t.TempDir(), "package.zip")
file, err := os.Create(packagePath)
if err != nil {
t.Fatalf("create update package: %v", err)
}
writer := zip.NewWriter(file)
for name, content := range map[string][]byte{
"artifact-selection.json": manifestContent,
"glory-soft-yms-test.jar": jar,
} {
entry, err := writer.Create(name)
if err != nil {
t.Fatalf("create update package entry: %v", err)
}
if _, err := entry.Write(content); err != nil {
t.Fatalf("write update package entry: %v", err)
}
}
if err := writer.Close(); err != nil {
t.Fatalf("close update package writer: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close update package: %v", err)
}
return packagePath
}
func writeDirectBackendJAR(t *testing.T, content []byte) string {
t.Helper()
jarPath := filepath.Join(t.TempDir(), "glory-soft-yms.jar")
file, err := os.Create(jarPath)
if err != nil {
t.Fatalf("create direct backend JAR: %v", err)
}
writer := zip.NewWriter(file)
entry, err := writer.Create("BOOT-INF/classes/application.properties")
if err != nil {
t.Fatalf("create direct backend JAR entry: %v", err)
}
if _, err := entry.Write(content); err != nil {
t.Fatalf("write direct backend JAR entry: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close direct backend JAR writer: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close direct backend JAR: %v", err)
}
return jarPath
}