feat: backend native executor implement
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user