2026-08-15 20:58:01 +08:00
|
|
|
package backendexecutor
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"slices"
|
2026-08-17 02:10:10 +08:00
|
|
|
"strings"
|
2026-08-15 20:58:01 +08:00
|
|
|
"sync"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"yms-daemon/internal/containerengine"
|
|
|
|
|
"yms-daemon/internal/transaction"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-08-17 10:10:14 +08:00
|
|
|
// testDigest 测试中使用的固定镜像清单摘要。
|
|
|
|
|
testDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
|
|
|
// testRepository 测试中使用的镜像仓库前缀。
|
|
|
|
|
testRepository = "harbor.ymswell.asia/ymswell/glory-ymswell"
|
|
|
|
|
// healthyResponse 测试中健康检查端点返回的固定 UP 响应体。
|
2026-08-15 20:58:01 +08:00
|
|
|
healthyResponse = `{"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"},"ping":{"status":"UP"},"redis":{"status":"UP"}}}`
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// TestExecutorPreservesOpaqueImageTagsAndReachesSwitching 验证执行器对不透明镜像标签保持原样传递,
|
|
|
|
|
// 并能使事务推进到 StateSwitching;同时验证在 StateSwitching 状态重复执行不会产生额外引擎调用。
|
2026-08-15 20:58:01 +08:00
|
|
|
func TestExecutorPreservesOpaqueImageTagsAndReachesSwitching(t *testing.T) {
|
|
|
|
|
tags := []string{
|
|
|
|
|
"20260814-093609-d7ed70f0-v1.1.8.1",
|
|
|
|
|
"20260814-093609-d7ed70f0",
|
|
|
|
|
}
|
|
|
|
|
for _, tag := range tags {
|
|
|
|
|
tag := tag
|
|
|
|
|
t.Run(tag, func(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
store, coordinator := testTransactionKernel(t)
|
|
|
|
|
imageReference := testRepository + ":" + tag
|
|
|
|
|
request := testRequest(t, imageReference)
|
|
|
|
|
engine := newFakeEngine(request)
|
|
|
|
|
executor := testExecutor(t, store, coordinator, engine, healthyResponse)
|
|
|
|
|
record := createTransaction(t, store, "opaque-"+tag)
|
|
|
|
|
|
|
|
|
|
if err := executor.Run(ctx, record.ID, request); err != nil {
|
|
|
|
|
t.Fatalf("run backend executor: %v", err)
|
|
|
|
|
}
|
|
|
|
|
current, err := store.Transaction(ctx, record.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("read completed preparation: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if current.State != transaction.StateSwitching {
|
|
|
|
|
t.Fatalf("unexpected transaction state: %s", current.State)
|
|
|
|
|
}
|
|
|
|
|
pending, err := store.PendingSteps(ctx, record.ID)
|
|
|
|
|
if err != nil || len(pending) != 0 {
|
|
|
|
|
t.Fatalf("unexpected pending steps: steps=%+v err=%v", pending, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
engine.mu.Lock()
|
|
|
|
|
if engine.lastCreateSpec.ImageReference != imageReference {
|
|
|
|
|
engine.mu.Unlock()
|
|
|
|
|
t.Fatalf("image reference changed: got %q want %q", engine.lastCreateSpec.ImageReference, imageReference)
|
|
|
|
|
}
|
2026-08-16 17:12:06 +08:00
|
|
|
if !slices.Contains(engine.lastCreateSpec.Environment, "SERVER_PORT=8081") || !slices.Contains(engine.lastCreateSpec.Environment, "SPRING_CONFIG_LOCATION=file:/app/config/yms.yaml") {
|
2026-08-15 20:58:01 +08:00
|
|
|
engine.mu.Unlock()
|
|
|
|
|
t.Fatalf("missing explicit port environment: %+v", engine.lastCreateSpec.Environment)
|
|
|
|
|
}
|
|
|
|
|
initialCalls := engine.callCounts()
|
|
|
|
|
engine.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if err := executor.Run(ctx, record.ID, request); err != nil {
|
|
|
|
|
t.Fatalf("repeat executor at switching state: %v", err)
|
|
|
|
|
}
|
|
|
|
|
engine.mu.Lock()
|
|
|
|
|
repeatedCalls := engine.callCounts()
|
|
|
|
|
engine.mu.Unlock()
|
|
|
|
|
if repeatedCalls != initialCalls {
|
|
|
|
|
t.Fatalf("switching state repeated engine calls: before=%+v after=%+v", initialCalls, repeatedCalls)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate 验证在 crash 窗口内已记录创建步骤意图后,
|
|
|
|
|
// 恢复执行不会重复创建容器,而是直接复用已完成的创建步骤并最终进入 StateSwitching。
|
2026-08-15 20:58:01 +08:00
|
|
|
func TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
store, coordinator := testTransactionKernel(t)
|
|
|
|
|
request := testRequest(t, testRepository+":20260814-093609-d7ed70f0-v1.1.8.1")
|
|
|
|
|
engine := newFakeEngine(request)
|
|
|
|
|
engine.imageAvailable = true
|
|
|
|
|
engine.containers[request.ContainerName] = engine.containerFromSpec(containerSpec(request), false)
|
|
|
|
|
executor := testExecutor(t, store, coordinator, engine, healthyResponse)
|
|
|
|
|
record := createTransaction(t, store, "recover-create")
|
|
|
|
|
transitionToPrepared(t, store, record.ID)
|
|
|
|
|
|
|
|
|
|
loadStep := loadIntent(request)
|
|
|
|
|
if _, _, err := store.RecordStepIntent(ctx, record.ID, loadStep); err != nil {
|
|
|
|
|
t.Fatalf("record completed image intent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := store.CompleteStep(ctx, record.ID, loadStep.Key, transaction.StepSucceeded, []byte(`{"loaded":true}`), ""); err != nil {
|
|
|
|
|
t.Fatalf("complete image step: %v", err)
|
|
|
|
|
}
|
2026-08-16 17:12:06 +08:00
|
|
|
removeStep := removeIntent(request)
|
|
|
|
|
if _, _, err := store.RecordStepIntent(ctx, record.ID, removeStep); err != nil {
|
|
|
|
|
t.Fatalf("record completed remove intent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := store.CompleteStep(ctx, record.ID, removeStep.Key, transaction.StepSucceeded, []byte(`{"absent":true}`), ""); err != nil {
|
|
|
|
|
t.Fatalf("complete remove step: %v", err)
|
|
|
|
|
}
|
2026-08-15 20:58:01 +08:00
|
|
|
createStep := createIntent(request, engine.loadedImage.ID)
|
|
|
|
|
if _, _, err := store.RecordStepIntent(ctx, record.ID, createStep); err != nil {
|
|
|
|
|
t.Fatalf("record crash-window create intent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := executor.Run(ctx, record.ID, request); err != nil {
|
|
|
|
|
t.Fatalf("recover backend executor: %v", err)
|
|
|
|
|
}
|
|
|
|
|
engine.mu.Lock()
|
|
|
|
|
calls := engine.callCounts()
|
|
|
|
|
engine.mu.Unlock()
|
2026-08-16 17:12:06 +08:00
|
|
|
if calls.load != 0 || calls.remove != 0 || calls.create != 0 || calls.start != 1 {
|
2026-08-15 20:58:01 +08:00
|
|
|
t.Fatalf("unexpected recovery calls: %+v", calls)
|
|
|
|
|
}
|
|
|
|
|
current, err := store.Transaction(ctx, record.ID)
|
|
|
|
|
if err != nil || current.State != transaction.StateSwitching {
|
|
|
|
|
t.Fatalf("unexpected recovered state: record=%+v err=%v", current, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// TestExecutorMarksValidationFailureTerminal 验证请求校验失败时事务被置为 StateFailed 终态。
|
2026-08-15 20:58:01 +08:00
|
|
|
func TestExecutorMarksValidationFailureTerminal(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
store, coordinator := testTransactionKernel(t)
|
|
|
|
|
request := testRequest(t, testRepository+":20260814-093609-d7ed70f0")
|
|
|
|
|
request.Port = 9090
|
|
|
|
|
engine := newFakeEngine(request)
|
|
|
|
|
executor := testExecutor(t, store, coordinator, engine, healthyResponse)
|
|
|
|
|
record := createTransaction(t, store, "invalid-port")
|
|
|
|
|
|
|
|
|
|
if err := executor.Run(ctx, record.ID, request); err == nil {
|
|
|
|
|
t.Fatal("expected validation failure")
|
|
|
|
|
}
|
|
|
|
|
current, err := store.Transaction(ctx, record.ID)
|
|
|
|
|
if err != nil || current.State != transaction.StateFailed {
|
|
|
|
|
t.Fatalf("unexpected failed transaction: record=%+v err=%v", current, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// TestExecutorReplacesInactiveContainerWithConflictingImage 验证当存在镜像标识冲突的非活动容器时,
|
|
|
|
|
// 执行器会移除该容器并重新创建,最终进入 StateSwitching。
|
2026-08-16 17:12:06 +08:00
|
|
|
func TestExecutorReplacesInactiveContainerWithConflictingImage(t *testing.T) {
|
2026-08-15 20:58:01 +08:00
|
|
|
ctx := context.Background()
|
|
|
|
|
store, coordinator := testTransactionKernel(t)
|
|
|
|
|
request := testRequest(t, testRepository+":20260814-093609-d7ed70f0")
|
|
|
|
|
engine := newFakeEngine(request)
|
|
|
|
|
engine.imageAvailable = true
|
|
|
|
|
conflicting := engine.containerFromSpec(containerSpec(request), false)
|
|
|
|
|
conflicting.ImageID = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
|
|
|
engine.containers[request.ContainerName] = conflicting
|
|
|
|
|
executor := testExecutor(t, store, coordinator, engine, healthyResponse)
|
|
|
|
|
record := createTransaction(t, store, "container-conflict")
|
|
|
|
|
|
2026-08-16 17:12:06 +08:00
|
|
|
if err := executor.Run(ctx, record.ID, request); err != nil {
|
|
|
|
|
t.Fatalf("replace inactive container: %v", err)
|
2026-08-15 20:58:01 +08:00
|
|
|
}
|
|
|
|
|
current, readErr := store.Transaction(ctx, record.ID)
|
2026-08-16 17:12:06 +08:00
|
|
|
if readErr != nil || current.State != transaction.StateSwitching {
|
|
|
|
|
t.Fatalf("replacement did not reach switching: record=%+v err=%v", current, readErr)
|
|
|
|
|
}
|
|
|
|
|
engine.mu.Lock()
|
|
|
|
|
calls := engine.callCounts()
|
|
|
|
|
engine.mu.Unlock()
|
|
|
|
|
if calls.remove != 1 || calls.create != 1 {
|
|
|
|
|
t.Fatalf("inactive replacement calls mismatch: %+v", calls)
|
2026-08-15 20:58:01 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// TestExecutorRejectsChangedRecoveryRequestWithoutChangingState 验证恢复执行时若请求参数被改动,
|
|
|
|
|
// 执行器会因持久化意图冲突而拒绝,且不改变事务的 StateStarting 状态。
|
2026-08-15 20:58:01 +08:00
|
|
|
func TestExecutorRejectsChangedRecoveryRequestWithoutChangingState(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
store, coordinator := testTransactionKernel(t)
|
|
|
|
|
request := testRequest(t, testRepository+":20260814-093609-d7ed70f0-v1.1.8.1")
|
|
|
|
|
engine := newFakeEngine(request)
|
|
|
|
|
engine.imageAvailable = true
|
|
|
|
|
executor := testExecutor(t, store, coordinator, engine, healthyResponse)
|
|
|
|
|
record := createTransaction(t, store, "changed-recovery-request")
|
|
|
|
|
transitionToPrepared(t, store, record.ID)
|
|
|
|
|
if err := executor.prepare(ctx, record.ID, request); err != nil {
|
|
|
|
|
t.Fatalf("prepare original request: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := store.Transition(ctx, record.ID, transaction.StateStarting, "test starting"); err != nil {
|
|
|
|
|
t.Fatalf("transition to starting: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changed := request
|
|
|
|
|
changed.ImageReference = testRepository + ":20260814-093609-d7ed70f0"
|
|
|
|
|
err := executor.Run(ctx, record.ID, changed)
|
|
|
|
|
if !errors.Is(err, transaction.ErrStepConflict) {
|
|
|
|
|
t.Fatalf("expected persisted intent conflict, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
current, readErr := store.Transaction(ctx, record.ID)
|
|
|
|
|
if readErr != nil || current.State != transaction.StateStarting {
|
|
|
|
|
t.Fatalf("changed recovery request altered transaction: record=%+v err=%v", current, readErr)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// TestImageMatchesRequiresDigestEvidenceAndExactPlatform 验证镜像匹配逻辑要求存在摘要证据且平台完全一致,
|
|
|
|
|
// 镜像 ID 单独存在不足以满足清单摘要匹配,平台不一致时不得判定匹配。
|
2026-08-15 20:58:01 +08:00
|
|
|
func TestImageMatchesRequiresDigestEvidenceAndExactPlatform(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
platform := containerengine.Platform{OS: "linux", Architecture: "arm64"}
|
|
|
|
|
image := containerengine.Image{
|
|
|
|
|
ID: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
|
|
|
RepoDigests: []string{testRepository + "@" + testDigest},
|
|
|
|
|
Platform: platform,
|
|
|
|
|
}
|
|
|
|
|
matched, err := imageMatches(image, testDigest, platform)
|
|
|
|
|
if err != nil || !matched {
|
|
|
|
|
t.Fatalf("expected exact image match: matched=%t err=%v", matched, err)
|
|
|
|
|
}
|
|
|
|
|
image.RepoDigests = nil
|
|
|
|
|
matched, err = imageMatches(image, testDigest, platform)
|
|
|
|
|
if err != nil || matched {
|
|
|
|
|
t.Fatalf("image ID alone must not satisfy manifest digest: matched=%t err=%v", matched, err)
|
|
|
|
|
}
|
|
|
|
|
image.DescriptorDigest = testDigest
|
|
|
|
|
matched, err = imageMatches(image, testDigest, containerengine.Platform{OS: "linux", Architecture: "amd64"})
|
|
|
|
|
if err != nil || matched {
|
|
|
|
|
t.Fatalf("platform mismatch accepted: matched=%t err=%v", matched, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// testRequest 构造一个合法的测试请求,并在临时目录中写入镜像归档与后端配置文件。
|
2026-08-15 20:58:01 +08:00
|
|
|
func testRequest(t *testing.T, imageReference string) Request {
|
|
|
|
|
t.Helper()
|
|
|
|
|
directory := t.TempDir()
|
|
|
|
|
archivePath := filepath.Join(directory, "backend-image.tar")
|
|
|
|
|
configPath := filepath.Join(directory, "yms.yaml")
|
|
|
|
|
if err := os.WriteFile(archivePath, []byte("image archive"), 0o600); err != nil {
|
|
|
|
|
t.Fatalf("write image archive: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(configPath, []byte("server: {}\n"), 0o600); err != nil {
|
|
|
|
|
t.Fatalf("write backend configuration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return Request{
|
2026-08-16 17:12:06 +08:00
|
|
|
ImageAcquisition: ImageAcquisitionLoad,
|
|
|
|
|
ArchivePath: archivePath,
|
|
|
|
|
ImageReference: imageReference,
|
|
|
|
|
ExpectedImageDigest: testDigest,
|
|
|
|
|
Platform: containerengine.Platform{OS: "linux", Architecture: "amd64"},
|
|
|
|
|
ContainerName: "explicit-backend-8081",
|
|
|
|
|
Port: 8081,
|
|
|
|
|
PortEnvironmentKey: "SERVER_PORT",
|
|
|
|
|
ConfigSource: configPath,
|
|
|
|
|
ConfigTarget: "/app/config/yms.yaml",
|
|
|
|
|
TmpSource: directory,
|
|
|
|
|
TmpTarget: "/home/yms/tmp",
|
|
|
|
|
ConfigEnvironmentKey: "SPRING_CONFIG_LOCATION",
|
|
|
|
|
ConfigLocation: "file:/app/config/yms.yaml",
|
|
|
|
|
RestartPolicy: containerengine.RestartPolicy{Name: "unless-stopped"},
|
|
|
|
|
HealthEndpoint: "http://127.0.0.1:8081/yms/actuator/health",
|
2026-08-15 20:58:01 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// testTransactionKernel 创建测试用的事务存储与协调器,并在测试结束时自动关闭存储。
|
2026-08-15 20:58:01 +08:00
|
|
|
func testTransactionKernel(t *testing.T) (*transaction.Store, *transaction.Coordinator) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
store, err := transaction.OpenStore(context.Background(), filepath.Join(t.TempDir(), "transactions.db"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("open transaction store: %v", err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
|
|
|
coordinator, err := transaction.NewCoordinator(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("create transaction coordinator: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return store, coordinator
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// testExecutor 使用固定响应体构造健康检查 HTTP 客户端并创建后端执行器。
|
2026-08-15 20:58:01 +08:00
|
|
|
func testExecutor(t *testing.T, store *transaction.Store, coordinator *transaction.Coordinator, engine containerengine.Engine, body string) *Executor {
|
|
|
|
|
t.Helper()
|
|
|
|
|
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
|
|
|
return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(bytes.NewBufferString(body))}, nil
|
|
|
|
|
})}
|
|
|
|
|
executor, err := New(store, coordinator, engine, client)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("create backend executor: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return executor
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// createTransaction 以给定后缀创建一个新的后端测试事务并返回其记录。
|
2026-08-15 20:58:01 +08:00
|
|
|
func createTransaction(t *testing.T, store *transaction.Store, suffix string) transaction.Transaction {
|
|
|
|
|
t.Helper()
|
|
|
|
|
record, _, err := store.CreateTransaction(context.Background(), transaction.CreateRequest{
|
|
|
|
|
ID: "backend-" + suffix,
|
|
|
|
|
IdempotencyKey: "backend-request-" + suffix,
|
|
|
|
|
Source: "test",
|
|
|
|
|
Service: "backend",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("create backend transaction: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return record
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// transitionToPrepared 将指定事务依次推进到 StateValidating 与 StatePrepared 状态。
|
2026-08-15 20:58:01 +08:00
|
|
|
func transitionToPrepared(t *testing.T, store *transaction.Store, transactionID string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
if _, err := store.Transition(ctx, transactionID, transaction.StateValidating, "test validating"); err != nil {
|
|
|
|
|
t.Fatalf("transition to validating: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := store.Transition(ctx, transactionID, transaction.StatePrepared, "test prepared"); err != nil {
|
|
|
|
|
t.Fatalf("transition to prepared: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// roundTripFunc http.RoundTripper 的函数式适配器,用于在测试中固定 HTTP 响应。
|
2026-08-15 20:58:01 +08:00
|
|
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// RoundTrip 实现 http.RoundTripper 接口,直接调用底层函数。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
|
|
|
return f(request)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// engineCalls 记录 fakeEngine 各方法被调用的次数,用于断言执行路径。
|
2026-08-15 20:58:01 +08:00
|
|
|
type engineCalls struct {
|
|
|
|
|
ping int
|
|
|
|
|
load int
|
|
|
|
|
inspect int
|
|
|
|
|
create int
|
|
|
|
|
start int
|
|
|
|
|
remove int
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// fakeEngine containerengine.Engine 的内存实现,用于测试中模拟容器引擎行为。
|
2026-08-15 20:58:01 +08:00
|
|
|
type fakeEngine struct {
|
2026-08-17 10:10:14 +08:00
|
|
|
// mu 保护下方字段的并发访问。
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
// request 构造该引擎时使用的请求参数。
|
|
|
|
|
request Request
|
|
|
|
|
// loadedImage 引擎加载后提供的镜像。
|
|
|
|
|
loadedImage containerengine.Image
|
|
|
|
|
// imageAvailable 表示镜像当前是否可用。
|
2026-08-15 20:58:01 +08:00
|
|
|
imageAvailable bool
|
2026-08-17 10:10:14 +08:00
|
|
|
// containers 引擎维护的容器表,键为容器名称。
|
|
|
|
|
containers map[string]containerengine.Container
|
|
|
|
|
// lastCreateSpec 记录最近一次创建容器所用的规格。
|
2026-08-15 20:58:01 +08:00
|
|
|
lastCreateSpec containerengine.ContainerSpec
|
|
|
|
|
pingCalls int
|
|
|
|
|
loadCalls int
|
|
|
|
|
inspectCalls int
|
|
|
|
|
createCalls int
|
|
|
|
|
startCalls int
|
|
|
|
|
removeCalls int
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// newFakeEngine 构造一个已按请求平台预置镜像的 fakeEngine。
|
2026-08-15 20:58:01 +08:00
|
|
|
func newFakeEngine(request Request) *fakeEngine {
|
|
|
|
|
return &fakeEngine{
|
|
|
|
|
request: request,
|
|
|
|
|
loadedImage: containerengine.Image{
|
|
|
|
|
ID: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
|
|
|
RepoDigests: []string{testRepository + "@" + testDigest},
|
|
|
|
|
Platform: request.Platform,
|
|
|
|
|
},
|
|
|
|
|
containers: make(map[string]containerengine.Container),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// Ping 实现容器引擎的 Ping,记录调用次数并始终返回成功。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) Ping(context.Context) error {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
e.pingCalls++
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// LoadImage 实现容器引擎的 LoadImage,读取输入流后标记镜像可用并记录调用次数。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) LoadImage(_ context.Context, input io.Reader) error {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
e.loadCalls++
|
|
|
|
|
if _, err := io.ReadAll(input); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.imageAvailable = true
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// PullImage 实现容器引擎的 PullImage,标记镜像可用并记录调用次数。
|
2026-08-16 17:12:06 +08:00
|
|
|
func (e *fakeEngine) PullImage(context.Context, string) error {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
e.loadCalls++
|
|
|
|
|
e.imageAvailable = true
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// InspectImage 实现容器引擎的 InspectImage,镜像不可用时返回 ErrNotFound,否则返回已加载镜像。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) InspectImage(context.Context, string) (containerengine.Image, error) {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
e.inspectCalls++
|
|
|
|
|
if !e.imageAvailable {
|
|
|
|
|
return containerengine.Image{}, containerengine.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return e.loadedImage, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// CreateContainer 实现容器引擎的 CreateContainer,记录规格并创建容器,同名时返回错误。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) CreateContainer(_ context.Context, spec containerengine.ContainerSpec) (containerengine.Container, error) {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
e.createCalls++
|
|
|
|
|
e.lastCreateSpec = spec
|
|
|
|
|
if _, exists := e.containers[spec.Name]; exists {
|
|
|
|
|
return containerengine.Container{}, errors.New("container name already exists")
|
|
|
|
|
}
|
|
|
|
|
record := e.containerFromSpec(spec, false)
|
|
|
|
|
e.containers[spec.Name] = record
|
|
|
|
|
return record, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// StartContainer 实现容器引擎的 StartContainer,将指定容器标记为运行状态,不存在时返回 ErrNotFound。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) StartContainer(_ context.Context, name string) error {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
e.startCalls++
|
|
|
|
|
record, exists := e.containers[name]
|
|
|
|
|
if !exists {
|
|
|
|
|
return containerengine.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
record.Running = true
|
|
|
|
|
record.Status = "running"
|
|
|
|
|
e.containers[name] = record
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// ContainerLogs 实现容器引擎的 ContainerLogs,返回空的日志读取器。
|
2026-08-17 02:10:10 +08:00
|
|
|
func (e *fakeEngine) ContainerLogs(context.Context, string) (io.ReadCloser, error) {
|
|
|
|
|
return io.NopCloser(strings.NewReader("")), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// StopContainer 实现容器引擎的 StopContainer,将指定容器标记为已退出状态,不存在时返回 ErrNotFound。
|
2026-08-22 15:34:09 +08:00
|
|
|
func (e *fakeEngine) StopContainer(_ context.Context, name string, _ int) error {
|
2026-08-16 17:12:06 +08:00
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
record, exists := e.containers[name]
|
|
|
|
|
if !exists {
|
|
|
|
|
return containerengine.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
record.Running = false
|
|
|
|
|
record.Status = "exited"
|
|
|
|
|
e.containers[name] = record
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// InspectContainer 实现容器引擎的 InspectContainer,返回容器记录,不存在时返回 ErrNotFound。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) InspectContainer(_ context.Context, name string) (containerengine.Container, error) {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
record, exists := e.containers[name]
|
|
|
|
|
if !exists {
|
|
|
|
|
return containerengine.Container{}, containerengine.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return record, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// RemoveContainer 实现容器引擎的 RemoveContainer,删除指定容器并记录调用次数,不存在时返回 ErrNotFound。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) RemoveContainer(_ context.Context, name string, _ bool) error {
|
|
|
|
|
e.mu.Lock()
|
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
e.removeCalls++
|
|
|
|
|
if _, exists := e.containers[name]; !exists {
|
|
|
|
|
return containerengine.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
delete(e.containers, name)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// Close 实现容器引擎的 Close,不做任何清理并返回 nil。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) Close() error { return nil }
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// containerFromSpec 根据容器规格构造一条容器记录,running 指定其初始运行状态。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) containerFromSpec(spec containerengine.ContainerSpec, running bool) containerengine.Container {
|
|
|
|
|
return containerengine.Container{
|
2026-08-16 17:12:06 +08:00
|
|
|
ID: "container-id-" + spec.Name,
|
|
|
|
|
Name: spec.Name,
|
|
|
|
|
ImageID: e.loadedImage.ID,
|
|
|
|
|
ImageReference: spec.ImageReference,
|
|
|
|
|
Platform: spec.Platform.OS + "/" + spec.Platform.Architecture,
|
|
|
|
|
Running: running,
|
|
|
|
|
Status: "created",
|
|
|
|
|
Environment: append([]string(nil), spec.Environment...),
|
|
|
|
|
NetworkMode: spec.NetworkMode,
|
|
|
|
|
RestartPolicy: spec.RestartPolicy,
|
|
|
|
|
Mounts: append([]containerengine.Mount(nil), spec.Mounts...),
|
|
|
|
|
User: spec.User,
|
|
|
|
|
StopTimeoutSeconds: spec.StopTimeoutSeconds,
|
2026-08-15 20:58:01 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// callCounts 返回 fakeEngine 各方法当前的调用次数快照。
|
2026-08-15 20:58:01 +08:00
|
|
|
func (e *fakeEngine) callCounts() engineCalls {
|
|
|
|
|
return engineCalls{
|
|
|
|
|
ping: e.pingCalls,
|
|
|
|
|
load: e.loadCalls,
|
|
|
|
|
inspect: e.inspectCalls,
|
|
|
|
|
create: e.createCalls,
|
|
|
|
|
start: e.startCalls,
|
|
|
|
|
remove: e.removeCalls,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// 编译期断言确保 fakeEngine 实现 containerengine.Engine 接口。
|
2026-08-15 20:58:01 +08:00
|
|
|
var _ containerengine.Engine = (*fakeEngine)(nil)
|