Files
yms-daemon/integration/native_backend_executor_linux_test.go
T

201 lines
6.7 KiB
Go

//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)