feat: backend native executor implement
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
//go:build linux
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"yms-daemon/internal/filestore"
|
||||
"yms-daemon/internal/nativebackendexecutor"
|
||||
"yms-daemon/internal/systemd"
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
func TestNativeBackendExecutorIntegration(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
root := t.TempDir()
|
||||
databasePath := filepath.Join(root, "transactions.db")
|
||||
store, err := transaction.OpenStore(ctx, databasePath)
|
||||
if err != nil {
|
||||
t.Fatalf("open transaction store: %v", err)
|
||||
}
|
||||
coordinator, err := transaction.NewCoordinator(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction coordinator: %v", err)
|
||||
}
|
||||
releaseStore, err := filestore.New(filepath.Join(root, "releases"))
|
||||
if err != nil {
|
||||
t.Fatalf("create native backend release store: %v", err)
|
||||
}
|
||||
|
||||
artifactContent := []byte("integrated native backend jar")
|
||||
artifactPath := filepath.Join(root, "incoming", "glory-soft-yms.jar")
|
||||
if err := os.MkdirAll(filepath.Dir(artifactPath), 0o750); err != nil {
|
||||
t.Fatalf("create incoming directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(artifactPath, artifactContent, 0o640); err != nil {
|
||||
t.Fatalf("write incoming backend JAR: %v", err)
|
||||
}
|
||||
previousTarget := filepath.Join(root, "previous", "glory-soft-yms.jar")
|
||||
if err := os.MkdirAll(filepath.Dir(previousTarget), 0o750); err != nil {
|
||||
t.Fatalf("create previous release directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(previousTarget, []byte("previous native backend jar"), 0o640); err != nil {
|
||||
t.Fatalf("write previous backend JAR: %v", err)
|
||||
}
|
||||
slotJarPath := filepath.Join(root, "slots", "backend-green.jar")
|
||||
if err := os.MkdirAll(filepath.Dir(slotJarPath), 0o750); err != nil {
|
||||
t.Fatalf("create native backend slot directory: %v", err)
|
||||
}
|
||||
if err := os.Symlink(previousTarget, slotJarPath); err != nil {
|
||||
t.Fatalf("create native backend slot link: %v", err)
|
||||
}
|
||||
|
||||
request := nativebackendexecutor.Request{
|
||||
ArtifactPath: artifactPath,
|
||||
ArtifactIdentity: integrationIdentity(artifactContent),
|
||||
ReleasePath: "glory-soft-yms-20260815.jar",
|
||||
SlotJarPath: slotJarPath,
|
||||
PreviousSlotTarget: previousTarget,
|
||||
UnitName: "yms-green.service",
|
||||
Port: 8081,
|
||||
HealthEndpoint: "http://127.0.0.1:8081/yms/actuator/health",
|
||||
}
|
||||
units := &integrationUnitManager{unit: systemd.Unit{
|
||||
Name: request.UnitName,
|
||||
LoadState: "loaded",
|
||||
ActiveState: "inactive",
|
||||
SubState: "dead",
|
||||
}}
|
||||
healthBody := `{"status":"UP","components":{"db":{"status":"UP","components":{"dorisDataSource":{"status":"UP"},"postgresqlDataSource":{"status":"UP"}}},"diskSpace":{"status":"UP"},"ping":{"status":"UP"},"redis":{"status":"UP"}}}`
|
||||
httpClient := &http.Client{Transport: integrationRoundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewBufferString(healthBody)),
|
||||
}, nil
|
||||
})}
|
||||
executor, err := nativebackendexecutor.New(store, coordinator, releaseStore, units, httpClient)
|
||||
if err != nil {
|
||||
t.Fatalf("create native backend executor: %v", err)
|
||||
}
|
||||
record, created, err := store.CreateTransaction(ctx, transaction.CreateRequest{
|
||||
ID: "native-backend-integration",
|
||||
IdempotencyKey: "native-backend-integration-request",
|
||||
Source: "integration-test",
|
||||
Service: "backend",
|
||||
})
|
||||
if err != nil || !created {
|
||||
t.Fatalf("create native backend transaction: record=%+v created=%t err=%v", record, created, err)
|
||||
}
|
||||
|
||||
if err := executor.Run(ctx, record.ID, request); err != nil {
|
||||
t.Fatalf("run native backend executor: %v", err)
|
||||
}
|
||||
installed, found, err := releaseStore.Inspect(request.ReleasePath, request.ArtifactIdentity)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("inspect installed backend JAR: file=%+v found=%t err=%v", installed, found, err)
|
||||
}
|
||||
actualContent, err := os.ReadFile(installed.Path)
|
||||
if err != nil || !bytes.Equal(actualContent, artifactContent) {
|
||||
t.Fatalf("verify installed backend JAR: content=%q err=%v", actualContent, err)
|
||||
}
|
||||
actualTarget, err := os.Readlink(slotJarPath)
|
||||
if err != nil || actualTarget != installed.Path {
|
||||
t.Fatalf("verify native backend slot link: target=%q installed=%q err=%v", actualTarget, installed.Path, err)
|
||||
}
|
||||
if err := store.Close(); err != nil {
|
||||
t.Fatalf("close transaction store: %v", err)
|
||||
}
|
||||
|
||||
reopened, err := transaction.OpenStore(ctx, databasePath)
|
||||
if err != nil {
|
||||
t.Fatalf("reopen transaction store: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = reopened.Close() })
|
||||
persisted, err := reopened.Transaction(ctx, record.ID)
|
||||
if err != nil || persisted.State != transaction.StateSwitching {
|
||||
t.Fatalf("unexpected persisted native transaction: record=%+v err=%v", persisted, err)
|
||||
}
|
||||
pending, err := reopened.PendingSteps(ctx, record.ID)
|
||||
if err != nil || len(pending) != 0 {
|
||||
t.Fatalf("unexpected persisted pending steps: steps=%+v err=%v", pending, err)
|
||||
}
|
||||
units.mu.Lock()
|
||||
startCalls := units.startCalls
|
||||
startedName := units.startedName
|
||||
units.mu.Unlock()
|
||||
if startCalls != 1 || startedName != request.UnitName {
|
||||
t.Fatalf("unexpected integrated systemd calls: start=%d name=%s", startCalls, startedName)
|
||||
}
|
||||
}
|
||||
|
||||
func integrationIdentity(content []byte) filestore.Identity {
|
||||
digest := sha256.Sum256(content)
|
||||
return filestore.Identity{Size: int64(len(content)), SHA256: hex.EncodeToString(digest[:])}
|
||||
}
|
||||
|
||||
type integrationUnitManager struct {
|
||||
mu sync.Mutex
|
||||
unit systemd.Unit
|
||||
startCalls int
|
||||
stopCalls int
|
||||
startedName string
|
||||
}
|
||||
|
||||
func (m *integrationUnitManager) Inspect(_ context.Context, name string) (systemd.Unit, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if name != m.unit.Name {
|
||||
return systemd.Unit{}, systemd.ErrUnitNotFound
|
||||
}
|
||||
return m.unit, nil
|
||||
}
|
||||
|
||||
func (m *integrationUnitManager) Start(_ context.Context, name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if name != m.unit.Name {
|
||||
return systemd.ErrUnitNotFound
|
||||
}
|
||||
m.startCalls++
|
||||
m.startedName = name
|
||||
m.unit.ActiveState = "active"
|
||||
m.unit.SubState = "running"
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *integrationUnitManager) Stop(_ context.Context, name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if name != m.unit.Name {
|
||||
return systemd.ErrUnitNotFound
|
||||
}
|
||||
m.stopCalls++
|
||||
m.unit.ActiveState = "inactive"
|
||||
m.unit.SubState = "dead"
|
||||
return nil
|
||||
}
|
||||
|
||||
type integrationRoundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f integrationRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.String() != "http://127.0.0.1:8081/yms/actuator/health" {
|
||||
return nil, errors.New("unexpected native backend health endpoint: " + request.URL.String())
|
||||
}
|
||||
return f(request)
|
||||
}
|
||||
|
||||
var _ systemd.Manager = (*integrationUnitManager)(nil)
|
||||
Reference in New Issue
Block a user