refactor: use nginx -s reload instead of systemd
- doc: add comment
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Package backendexecutor prepares and starts one explicitly named backend container.
|
||||
// Gateway switching is deliberately outside this package.
|
||||
// Package backendexecutor 负责准备并启动一个显式命名的后端容器。
|
||||
// 网关流量切换被刻意排除在本包职责之外。
|
||||
package backendexecutor
|
||||
|
||||
import (
|
||||
@@ -25,56 +25,95 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
healthPath = "/yms/actuator/health"
|
||||
healthTimeout = 120 * time.Second
|
||||
healthInterval = time.Second
|
||||
// healthPath 后端 Actuator 健康检查端点的固定路径。
|
||||
healthPath = "/yms/actuator/health"
|
||||
// healthTimeout 等待后端容器健康的整体超时时间。
|
||||
healthTimeout = 120 * time.Second
|
||||
// healthInterval Actuator 健康检查的轮询间隔。
|
||||
healthInterval = time.Second
|
||||
// containerStopTimeoutSeconds 停止后端容器时允许的最长等待秒数。
|
||||
containerStopTimeoutSeconds = 150 * 60
|
||||
hostNetworkMode = "host"
|
||||
bindMountType = "bind"
|
||||
stepLoadImage = "backend.image.load"
|
||||
stepPullImage = "backend.image.pull"
|
||||
stepRemoveContainer = "backend.container.remove-inactive"
|
||||
stepCreateContainer = "backend.container.create"
|
||||
stepStartContainer = "backend.container.start"
|
||||
stepCheckHealth = "backend.container.health"
|
||||
// hostNetworkMode 表示后端容器使用宿主机网络命名空间。
|
||||
hostNetworkMode = "host"
|
||||
// bindMountType 表示后端容器的挂载类型为绑定挂载。
|
||||
bindMountType = "bind"
|
||||
// stepLoadImage 从归档加载后端镜像的持久化步骤键。
|
||||
stepLoadImage = "backend.image.load"
|
||||
// stepPullImage 从仓库拉取后端镜像的持久化步骤键。
|
||||
stepPullImage = "backend.image.pull"
|
||||
// stepRemoveContainer 移除非活动后端容器的持久化步骤键。
|
||||
stepRemoveContainer = "backend.container.remove-inactive"
|
||||
// stepCreateContainer 创建非活动后端容器的持久化步骤键。
|
||||
stepCreateContainer = "backend.container.create"
|
||||
// stepStartContainer 启动非活动后端容器的持久化步骤键。
|
||||
stepStartContainer = "backend.container.start"
|
||||
// stepCheckHealth 等待后端 Actuator 健康的持久化步骤键。
|
||||
stepCheckHealth = "backend.container.health"
|
||||
)
|
||||
|
||||
const (
|
||||
// ImageAcquisitionLoad 表示通过本地归档加载方式获取后端镜像。
|
||||
ImageAcquisitionLoad = "load"
|
||||
// ImageAcquisitionPull 表示通过远程仓库拉取方式获取后端镜像。
|
||||
ImageAcquisitionPull = "pull"
|
||||
)
|
||||
|
||||
// Request contains exact values supplied by the update package and local deployment configuration.
|
||||
// ImageReference is opaque: the executor never extracts meaning from its tag.
|
||||
// Request 携带 update 包与本地部署配置提供的精确取值。
|
||||
// ImageReference 不透明值:执行器从不解析其标签中的任何含义。
|
||||
type Request struct {
|
||||
ImageAcquisition string
|
||||
ArchivePath string
|
||||
ImageReference string
|
||||
ExpectedImageDigest string
|
||||
Platform containerengine.Platform
|
||||
ContainerName string
|
||||
Port int
|
||||
PortEnvironmentKey string
|
||||
ConfigSource string
|
||||
ConfigTarget string
|
||||
TmpSource string
|
||||
TmpTarget string
|
||||
// ImageAcquisition 指定镜像获取方式,取值为 ImageAcquisitionLoad 或 ImageAcquisitionPull。
|
||||
ImageAcquisition string
|
||||
// ArchivePath 当 ImageAcquisition 为 ImageAcquisitionLoad 时本地镜像归档的绝对路径。
|
||||
ArchivePath string
|
||||
// ImageReference 后端镜像的精确引用,执行器不解析其语义。
|
||||
ImageReference string
|
||||
// ExpectedImageDigest 后端镜像期望的清单摘要。
|
||||
ExpectedImageDigest string
|
||||
// Platform 后端镜像显式指定的操作系统与架构。
|
||||
Platform containerengine.Platform
|
||||
// ContainerName 后端容器的精确名称。
|
||||
ContainerName string
|
||||
// Port 后端容器的监听端口,仅允许 8080 或 8081。
|
||||
Port int
|
||||
// PortEnvironmentKey 注入端口值的环境变量键。
|
||||
PortEnvironmentKey string
|
||||
// ConfigSource 宿主机上后端配置文件的绝对路径。
|
||||
ConfigSource string
|
||||
// ConfigTarget 后端配置在容器内的绝对挂载路径。
|
||||
ConfigTarget string
|
||||
// TmpSource 宿主机上后端临时目录的绝对路径。
|
||||
TmpSource string
|
||||
// TmpTarget 后端临时目录在容器内的绝对挂载路径。
|
||||
TmpTarget string
|
||||
// ConfigEnvironmentKey 注入配置位置的环境变量键。
|
||||
ConfigEnvironmentKey string
|
||||
ConfigLocation string
|
||||
RestartPolicy containerengine.RestartPolicy
|
||||
HealthEndpoint string
|
||||
StartLog bool
|
||||
LogReporter func(string)
|
||||
// ConfigLocation 后端配置在容器内的位置取值。
|
||||
ConfigLocation string
|
||||
// RestartPolicy 后端容器的重启策略。
|
||||
RestartPolicy containerengine.RestartPolicy
|
||||
// HealthEndpoint 后端 Actuator 健康检查的 HTTP 端点。
|
||||
HealthEndpoint string
|
||||
// StartLog 表示是否读取并回传容器启动日志。
|
||||
StartLog bool
|
||||
// LogReporter 用于回传容器启动日志的每一行。
|
||||
LogReporter func(string)
|
||||
}
|
||||
|
||||
// Executor drives the persisted transaction up to SWITCHING after the new container is healthy.
|
||||
// Executor 在新容器恢复健康后,将持久化事务推进到 StateSwitching 状态。
|
||||
type Executor struct {
|
||||
store *transaction.Store
|
||||
// store 持久化事务的存储。
|
||||
store *transaction.Store
|
||||
// coordinator 负责事务的独占执行与步骤执行。
|
||||
coordinator *transaction.Coordinator
|
||||
engine containerengine.Engine
|
||||
checker *healthcheck.ActuatorChecker
|
||||
// engine 底层容器引擎。
|
||||
engine containerengine.Engine
|
||||
// checker Actuator 健康检查器。
|
||||
checker *healthcheck.ActuatorChecker
|
||||
}
|
||||
|
||||
// New 构造一个 Executor,并校验所有必要依赖非空。
|
||||
// store 为事务存储,coordinator 为事务协调器,engine 为容器引擎,httpClient 用于健康检查。
|
||||
// 任一必要依赖为 nil 时返回错误;健康检查器构造失败时返回该错误。
|
||||
func New(store *transaction.Store, coordinator *transaction.Coordinator, engine containerengine.Engine, httpClient *http.Client) (*Executor, error) {
|
||||
if store == nil {
|
||||
return nil, errors.New("transaction store is required")
|
||||
@@ -92,7 +131,9 @@ func New(store *transaction.Store, coordinator *transaction.Coordinator, engine
|
||||
return &Executor{store: store, coordinator: coordinator, engine: engine, checker: checker}, nil
|
||||
}
|
||||
|
||||
// Run resumes from the transaction's persisted state. It does not switch gateway traffic.
|
||||
// Run 从事务的持久化状态恢复执行。它不会切换网关流量。
|
||||
// transactionID 待执行的事务标识,request 是本次执行携带的请求参数。
|
||||
// transactionID 为空时返回错误;其余错误来自事务协调器的独占执行。
|
||||
func (e *Executor) Run(ctx context.Context, transactionID string, request Request) error {
|
||||
if strings.TrimSpace(transactionID) == "" {
|
||||
return errors.New("transaction ID is required")
|
||||
@@ -102,6 +143,8 @@ func (e *Executor) Run(ctx context.Context, transactionID string, request Reques
|
||||
})
|
||||
}
|
||||
|
||||
// run 在独占执行上下文内的实现,循环读取事务状态并按状态推进,
|
||||
// 直到事务进入 StateSwitching 状态后返回。每个状态分支处理失败或推进错误时立即返回。
|
||||
func (e *Executor) run(ctx context.Context, transactionID string, request Request) error {
|
||||
for {
|
||||
record, err := e.store.Transaction(ctx, transactionID)
|
||||
@@ -147,6 +190,9 @@ func (e *Executor) run(ctx context.Context, transactionID string, request Reques
|
||||
}
|
||||
}
|
||||
|
||||
// failUnlessRecoverable 判断 prepare 阶段的错误是否可恢复。
|
||||
// 若 cause 是 UncertainStepError 或 ErrStepConflict,则直接原样返回(保留不确定性以便重放恢复);
|
||||
// 否则将事务标记为 StateFailed 并合并返回 cause 与状态迁移错误。
|
||||
func (e *Executor) failUnlessRecoverable(ctx context.Context, transactionID string, cause error) error {
|
||||
var uncertain *transaction.UncertainStepError
|
||||
if errors.As(cause, &uncertain) || errors.Is(cause, transaction.ErrStepConflict) {
|
||||
@@ -156,6 +202,8 @@ func (e *Executor) failUnlessRecoverable(ctx context.Context, transactionID stri
|
||||
return errors.Join(cause, transitionErr)
|
||||
}
|
||||
|
||||
// validate 对请求输入做运行时校验:先校验请求字段,再根据镜像获取方式校验镜像归档,
|
||||
// 随后校验配置文件为普通文件、临时目录为直接目录,最后对容器引擎执行 Ping 探活。
|
||||
func (e *Executor) validate(ctx context.Context, request Request) error {
|
||||
if err := validateRequest(request); err != nil {
|
||||
return err
|
||||
@@ -177,6 +225,8 @@ func (e *Executor) validate(ctx context.Context, request Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepare 根据镜像获取方式执行对应的加载或拉取步骤,随后核对镜像、移除旧的非活动容器,
|
||||
// 并创建新的非活动后端容器。所有步骤均通过事务协调器持久化执行以保证可恢复。
|
||||
func (e *Executor) prepare(ctx context.Context, transactionID string, request Request) error {
|
||||
switch request.ImageAcquisition {
|
||||
case ImageAcquisitionLoad:
|
||||
@@ -208,6 +258,8 @@ func (e *Executor) prepare(ctx context.Context, transactionID string, request Re
|
||||
return err
|
||||
}
|
||||
|
||||
// startAndCheck 先启动非活动后端容器,若请求要求读取启动日志则逐行回传,
|
||||
// 最后执行健康检查步骤等待后端 Actuator 健康。
|
||||
func (e *Executor) startAndCheck(ctx context.Context, transactionID string, request Request) error {
|
||||
startOperation := &startContainerOperation{engine: e.engine, name: request.ContainerName}
|
||||
if _, err := e.coordinator.ExecuteStep(ctx, transactionID, startIntent(request), startOperation); err != nil {
|
||||
@@ -239,6 +291,8 @@ func (e *Executor) startAndCheck(ctx context.Context, transactionID string, requ
|
||||
return err
|
||||
}
|
||||
|
||||
// validateRequest 对请求字段做静态校验,确保所有取值精确且自洽。
|
||||
// 任一字段不符合要求时返回描述性错误。
|
||||
func validateRequest(request Request) error {
|
||||
switch request.ImageAcquisition {
|
||||
case ImageAcquisitionLoad:
|
||||
@@ -295,6 +349,8 @@ func validateRequest(request Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRestartPolicy 校验重启策略名称与其最大重试次数的组合是否合法。
|
||||
// "no"、"always"、"unless-stopped" 不接受最大重试次数;"on-failure" 允许非负次数;其余名称不受支持。
|
||||
func validateRestartPolicy(policy containerengine.RestartPolicy) error {
|
||||
switch policy.Name {
|
||||
case "no", "always", "unless-stopped":
|
||||
@@ -311,6 +367,7 @@ func validateRestartPolicy(policy containerengine.RestartPolicy) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// regularFile 校验 path 指向一个普通文件,description 用于构造错误信息。
|
||||
func regularFile(path, description string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
@@ -322,6 +379,7 @@ func regularFile(path, description string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// directDirectory 校验 path 指向一个目录且不是符号链接(即“直接目录”)。
|
||||
func directDirectory(path, description string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
@@ -333,6 +391,8 @@ func directDirectory(path, description string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// containerSpec 根据请求构造后端容器的完整规格,包括名称、镜像引用、平台、环境变量、
|
||||
// 宿主机网络模式、重启策略、绑定挂载、用户与停止超时。
|
||||
func containerSpec(request Request) containerengine.ContainerSpec {
|
||||
return containerengine.ContainerSpec{
|
||||
Name: request.ContainerName,
|
||||
@@ -353,6 +413,7 @@ func containerSpec(request Request) containerengine.ContainerSpec {
|
||||
}
|
||||
}
|
||||
|
||||
// pullIntent 构造拉取并校验后端镜像步骤的持久化意图。
|
||||
func pullIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepPullImage, "pull and verify backend image", struct {
|
||||
ImageReference string `json:"imageReference"`
|
||||
@@ -361,6 +422,7 @@ func pullIntent(request Request) transaction.StepIntent {
|
||||
}{request.ImageReference, request.ExpectedImageDigest, request.Platform})
|
||||
}
|
||||
|
||||
// loadIntent 构造从归档加载并校验后端镜像步骤的持久化意图。
|
||||
func loadIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepLoadImage, "load and verify backend image", struct {
|
||||
ArchivePath string `json:"archivePath"`
|
||||
@@ -370,6 +432,7 @@ func loadIntent(request Request) transaction.StepIntent {
|
||||
}{request.ArchivePath, request.ImageReference, request.ExpectedImageDigest, request.Platform})
|
||||
}
|
||||
|
||||
// createIntent 构造创建非活动后端容器步骤的持久化意图,imageID 为已核对镜像的标识。
|
||||
func createIntent(request Request, imageID string) transaction.StepIntent {
|
||||
return intent(stepCreateContainer, "create inactive backend container", struct {
|
||||
Spec containerengine.ContainerSpec `json:"spec"`
|
||||
@@ -377,18 +440,21 @@ func createIntent(request Request, imageID string) transaction.StepIntent {
|
||||
}{containerSpec(request), imageID})
|
||||
}
|
||||
|
||||
// removeIntent 构造移除非活动后端容器步骤的持久化意图。
|
||||
func removeIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepRemoveContainer, "remove inactive backend container", struct {
|
||||
ContainerName string `json:"containerName"`
|
||||
}{request.ContainerName})
|
||||
}
|
||||
|
||||
// startIntent 构造启动非活动后端容器步骤的持久化意图。
|
||||
func startIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepStartContainer, "start inactive backend container", struct {
|
||||
ContainerName string `json:"containerName"`
|
||||
}{request.ContainerName})
|
||||
}
|
||||
|
||||
// healthIntent 构造等待后端 Actuator 健康步骤的持久化意图。
|
||||
func healthIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepCheckHealth, "wait for backend Actuator health", struct {
|
||||
ContainerName string `json:"containerName"`
|
||||
@@ -397,6 +463,7 @@ func healthIntent(request Request) transaction.StepIntent {
|
||||
}{request.ContainerName, request.HealthEndpoint, healthTimeout})
|
||||
}
|
||||
|
||||
// intent 将任意值序列化为 JSON 后封装成事务步骤意图。序列化失败视为内部错误并直接 panic。
|
||||
func intent(key, name string, value any) transaction.StepIntent {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
@@ -405,6 +472,9 @@ func intent(key, name string, value any) transaction.StepIntent {
|
||||
return transaction.StepIntent{Key: key, Name: name, Intent: payload}
|
||||
}
|
||||
|
||||
// imageMatches 判断已核对镜像是否与期望摘要及期望平台完全匹配。
|
||||
// 当镜像平台与期望平台不一致时直接返回 false;否则依据镜像的描述符摘要或仓库摘要
|
||||
// 中是否存在与期望摘要相等的证据来判定匹配。
|
||||
func imageMatches(image containerengine.Image, expectedDigest string, expectedPlatform containerengine.Platform) (bool, error) {
|
||||
expected, err := opencontainersdigest.Parse(expectedDigest)
|
||||
if err != nil {
|
||||
@@ -438,6 +508,7 @@ func imageMatches(image containerengine.Image, expectedDigest string, expectedPl
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// resultJSON 将任意值序列化为 JSON RawMessage。序列化失败视为内部错误并直接 panic。
|
||||
func resultJSON(value any) json.RawMessage {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,11 +19,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
testDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
testRepository = "harbor.ymswell.asia/ymswell/glory-ymswell"
|
||||
// testDigest 测试中使用的固定镜像清单摘要。
|
||||
testDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
// testRepository 测试中使用的镜像仓库前缀。
|
||||
testRepository = "harbor.ymswell.asia/ymswell/glory-ymswell"
|
||||
// healthyResponse 测试中健康检查端点返回的固定 UP 响应体。
|
||||
healthyResponse = `{"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"},"ping":{"status":"UP"},"redis":{"status":"UP"}}}`
|
||||
)
|
||||
|
||||
// TestExecutorPreservesOpaqueImageTagsAndReachesSwitching 验证执行器对不透明镜像标签保持原样传递,
|
||||
// 并能使事务推进到 StateSwitching;同时验证在 StateSwitching 状态重复执行不会产生额外引擎调用。
|
||||
func TestExecutorPreservesOpaqueImageTagsAndReachesSwitching(t *testing.T) {
|
||||
tags := []string{
|
||||
"20260814-093609-d7ed70f0-v1.1.8.1",
|
||||
@@ -80,6 +85,8 @@ func TestExecutorPreservesOpaqueImageTagsAndReachesSwitching(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate 验证在 crash 窗口内已记录创建步骤意图后,
|
||||
// 恢复执行不会重复创建容器,而是直接复用已完成的创建步骤并最终进入 StateSwitching。
|
||||
func TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, coordinator := testTransactionKernel(t)
|
||||
@@ -125,6 +132,7 @@ func TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorMarksValidationFailureTerminal 验证请求校验失败时事务被置为 StateFailed 终态。
|
||||
func TestExecutorMarksValidationFailureTerminal(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, coordinator := testTransactionKernel(t)
|
||||
@@ -143,6 +151,8 @@ func TestExecutorMarksValidationFailureTerminal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorReplacesInactiveContainerWithConflictingImage 验证当存在镜像标识冲突的非活动容器时,
|
||||
// 执行器会移除该容器并重新创建,最终进入 StateSwitching。
|
||||
func TestExecutorReplacesInactiveContainerWithConflictingImage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, coordinator := testTransactionKernel(t)
|
||||
@@ -170,6 +180,8 @@ func TestExecutorReplacesInactiveContainerWithConflictingImage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRejectsChangedRecoveryRequestWithoutChangingState 验证恢复执行时若请求参数被改动,
|
||||
// 执行器会因持久化意图冲突而拒绝,且不改变事务的 StateStarting 状态。
|
||||
func TestExecutorRejectsChangedRecoveryRequestWithoutChangingState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, coordinator := testTransactionKernel(t)
|
||||
@@ -198,6 +210,8 @@ func TestExecutorRejectsChangedRecoveryRequestWithoutChangingState(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestImageMatchesRequiresDigestEvidenceAndExactPlatform 验证镜像匹配逻辑要求存在摘要证据且平台完全一致,
|
||||
// 镜像 ID 单独存在不足以满足清单摘要匹配,平台不一致时不得判定匹配。
|
||||
func TestImageMatchesRequiresDigestEvidenceAndExactPlatform(t *testing.T) {
|
||||
t.Parallel()
|
||||
platform := containerengine.Platform{OS: "linux", Architecture: "arm64"}
|
||||
@@ -222,6 +236,7 @@ func TestImageMatchesRequiresDigestEvidenceAndExactPlatform(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// testRequest 构造一个合法的测试请求,并在临时目录中写入镜像归档与后端配置文件。
|
||||
func testRequest(t *testing.T, imageReference string) Request {
|
||||
t.Helper()
|
||||
directory := t.TempDir()
|
||||
@@ -253,6 +268,7 @@ func testRequest(t *testing.T, imageReference string) Request {
|
||||
}
|
||||
}
|
||||
|
||||
// testTransactionKernel 创建测试用的事务存储与协调器,并在测试结束时自动关闭存储。
|
||||
func testTransactionKernel(t *testing.T) (*transaction.Store, *transaction.Coordinator) {
|
||||
t.Helper()
|
||||
store, err := transaction.OpenStore(context.Background(), filepath.Join(t.TempDir(), "transactions.db"))
|
||||
@@ -267,6 +283,7 @@ func testTransactionKernel(t *testing.T) (*transaction.Store, *transaction.Coord
|
||||
return store, coordinator
|
||||
}
|
||||
|
||||
// testExecutor 使用固定响应体构造健康检查 HTTP 客户端并创建后端执行器。
|
||||
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) {
|
||||
@@ -279,6 +296,7 @@ func testExecutor(t *testing.T, store *transaction.Store, coordinator *transacti
|
||||
return executor
|
||||
}
|
||||
|
||||
// createTransaction 以给定后缀创建一个新的后端测试事务并返回其记录。
|
||||
func createTransaction(t *testing.T, store *transaction.Store, suffix string) transaction.Transaction {
|
||||
t.Helper()
|
||||
record, _, err := store.CreateTransaction(context.Background(), transaction.CreateRequest{
|
||||
@@ -293,6 +311,7 @@ func createTransaction(t *testing.T, store *transaction.Store, suffix string) tr
|
||||
return record
|
||||
}
|
||||
|
||||
// transitionToPrepared 将指定事务依次推进到 StateValidating 与 StatePrepared 状态。
|
||||
func transitionToPrepared(t *testing.T, store *transaction.Store, transactionID string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
@@ -304,12 +323,15 @@ func transitionToPrepared(t *testing.T, store *transaction.Store, transactionID
|
||||
}
|
||||
}
|
||||
|
||||
// roundTripFunc http.RoundTripper 的函数式适配器,用于在测试中固定 HTTP 响应。
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
// RoundTrip 实现 http.RoundTripper 接口,直接调用底层函数。
|
||||
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return f(request)
|
||||
}
|
||||
|
||||
// engineCalls 记录 fakeEngine 各方法被调用的次数,用于断言执行路径。
|
||||
type engineCalls struct {
|
||||
ping int
|
||||
load int
|
||||
@@ -319,12 +341,19 @@ type engineCalls struct {
|
||||
remove int
|
||||
}
|
||||
|
||||
// fakeEngine containerengine.Engine 的内存实现,用于测试中模拟容器引擎行为。
|
||||
type fakeEngine struct {
|
||||
mu sync.Mutex
|
||||
request Request
|
||||
loadedImage containerengine.Image
|
||||
// mu 保护下方字段的并发访问。
|
||||
mu sync.Mutex
|
||||
// request 构造该引擎时使用的请求参数。
|
||||
request Request
|
||||
// loadedImage 引擎加载后提供的镜像。
|
||||
loadedImage containerengine.Image
|
||||
// imageAvailable 表示镜像当前是否可用。
|
||||
imageAvailable bool
|
||||
containers map[string]containerengine.Container
|
||||
// containers 引擎维护的容器表,键为容器名称。
|
||||
containers map[string]containerengine.Container
|
||||
// lastCreateSpec 记录最近一次创建容器所用的规格。
|
||||
lastCreateSpec containerengine.ContainerSpec
|
||||
pingCalls int
|
||||
loadCalls int
|
||||
@@ -334,6 +363,7 @@ type fakeEngine struct {
|
||||
removeCalls int
|
||||
}
|
||||
|
||||
// newFakeEngine 构造一个已按请求平台预置镜像的 fakeEngine。
|
||||
func newFakeEngine(request Request) *fakeEngine {
|
||||
return &fakeEngine{
|
||||
request: request,
|
||||
@@ -346,6 +376,7 @@ func newFakeEngine(request Request) *fakeEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// Ping 实现容器引擎的 Ping,记录调用次数并始终返回成功。
|
||||
func (e *fakeEngine) Ping(context.Context) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -353,6 +384,7 @@ func (e *fakeEngine) Ping(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadImage 实现容器引擎的 LoadImage,读取输入流后标记镜像可用并记录调用次数。
|
||||
func (e *fakeEngine) LoadImage(_ context.Context, input io.Reader) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -364,6 +396,7 @@ func (e *fakeEngine) LoadImage(_ context.Context, input io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PullImage 实现容器引擎的 PullImage,标记镜像可用并记录调用次数。
|
||||
func (e *fakeEngine) PullImage(context.Context, string) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -372,6 +405,7 @@ func (e *fakeEngine) PullImage(context.Context, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InspectImage 实现容器引擎的 InspectImage,镜像不可用时返回 ErrNotFound,否则返回已加载镜像。
|
||||
func (e *fakeEngine) InspectImage(context.Context, string) (containerengine.Image, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -382,6 +416,7 @@ func (e *fakeEngine) InspectImage(context.Context, string) (containerengine.Imag
|
||||
return e.loadedImage, nil
|
||||
}
|
||||
|
||||
// CreateContainer 实现容器引擎的 CreateContainer,记录规格并创建容器,同名时返回错误。
|
||||
func (e *fakeEngine) CreateContainer(_ context.Context, spec containerengine.ContainerSpec) (containerengine.Container, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -395,6 +430,7 @@ func (e *fakeEngine) CreateContainer(_ context.Context, spec containerengine.Con
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// StartContainer 实现容器引擎的 StartContainer,将指定容器标记为运行状态,不存在时返回 ErrNotFound。
|
||||
func (e *fakeEngine) StartContainer(_ context.Context, name string) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -409,10 +445,12 @@ func (e *fakeEngine) StartContainer(_ context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContainerLogs 实现容器引擎的 ContainerLogs,返回空的日志读取器。
|
||||
func (e *fakeEngine) ContainerLogs(context.Context, string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
}
|
||||
|
||||
// StopContainer 实现容器引擎的 StopContainer,将指定容器标记为已退出状态,不存在时返回 ErrNotFound。
|
||||
func (e *fakeEngine) StopContainer(_ context.Context, name string) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -426,6 +464,7 @@ func (e *fakeEngine) StopContainer(_ context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InspectContainer 实现容器引擎的 InspectContainer,返回容器记录,不存在时返回 ErrNotFound。
|
||||
func (e *fakeEngine) InspectContainer(_ context.Context, name string) (containerengine.Container, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -436,6 +475,7 @@ func (e *fakeEngine) InspectContainer(_ context.Context, name string) (container
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// RemoveContainer 实现容器引擎的 RemoveContainer,删除指定容器并记录调用次数,不存在时返回 ErrNotFound。
|
||||
func (e *fakeEngine) RemoveContainer(_ context.Context, name string, _ bool) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -447,8 +487,10 @@ func (e *fakeEngine) RemoveContainer(_ context.Context, name string, _ bool) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 实现容器引擎的 Close,不做任何清理并返回 nil。
|
||||
func (e *fakeEngine) Close() error { return nil }
|
||||
|
||||
// containerFromSpec 根据容器规格构造一条容器记录,running 指定其初始运行状态。
|
||||
func (e *fakeEngine) containerFromSpec(spec containerengine.ContainerSpec, running bool) containerengine.Container {
|
||||
return containerengine.Container{
|
||||
ID: "container-id-" + spec.Name,
|
||||
@@ -467,6 +509,7 @@ func (e *fakeEngine) containerFromSpec(spec containerengine.ContainerSpec, runni
|
||||
}
|
||||
}
|
||||
|
||||
// callCounts 返回 fakeEngine 各方法当前的调用次数快照。
|
||||
func (e *fakeEngine) callCounts() engineCalls {
|
||||
return engineCalls{
|
||||
ping: e.pingCalls,
|
||||
@@ -478,4 +521,5 @@ func (e *fakeEngine) callCounts() engineCalls {
|
||||
}
|
||||
}
|
||||
|
||||
// 编译期断言确保 fakeEngine 实现 containerengine.Engine 接口。
|
||||
var _ containerengine.Engine = (*fakeEngine)(nil)
|
||||
|
||||
@@ -15,25 +15,39 @@ import (
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
// loadImageOperation 表示“从本地归档加载并校验后端镜像”的事务操作。
|
||||
type loadImageOperation struct {
|
||||
engine containerengine.Engine
|
||||
archivePath string
|
||||
// engine 执行加载操作的容器引擎。
|
||||
engine containerengine.Engine
|
||||
// archivePath 本地镜像归档的绝对路径。
|
||||
archivePath string
|
||||
// imageReference 镜像加载后的目标精确引用。
|
||||
imageReference string
|
||||
// expectedDigest 加载镜像后期望的清单摘要。
|
||||
expectedDigest string
|
||||
platform containerengine.Platform
|
||||
// platform 镜像的期望平台。
|
||||
platform containerengine.Platform
|
||||
}
|
||||
|
||||
// pullImageOperation 表示“从远程仓库拉取并校验后端镜像”的事务操作。
|
||||
type pullImageOperation struct {
|
||||
engine containerengine.Engine
|
||||
// engine 执行拉取操作的容器引擎。
|
||||
engine containerengine.Engine
|
||||
// imageReference 待拉取的镜像精确引用。
|
||||
imageReference string
|
||||
// expectedDigest 拉取镜像后期望的清单摘要。
|
||||
expectedDigest string
|
||||
platform containerengine.Platform
|
||||
// platform 镜像的期望平台。
|
||||
platform containerengine.Platform
|
||||
}
|
||||
|
||||
// Apply 调用容器引擎按 imageReference 拉取后端镜像。
|
||||
func (o *pullImageOperation) Apply(ctx context.Context) error {
|
||||
return o.engine.PullImage(ctx, o.imageReference)
|
||||
}
|
||||
|
||||
// Inspect 检查拉取操作是否已生效:镜像不存在时为未应用;存在时依据摘要与平台判定,
|
||||
// 匹配则返回已应用,否则返回未应用。无法判定或解析失败时返回错误。
|
||||
func (o *pullImageOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
image, err := o.engine.InspectImage(ctx, o.imageReference)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
@@ -53,6 +67,7 @@ func (o *pullImageOperation) Inspect(ctx context.Context) (transaction.Inspectio
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
}
|
||||
|
||||
// Apply 打开本地镜像归档并调用容器引擎加载后端镜像。
|
||||
func (o *loadImageOperation) Apply(ctx context.Context) error {
|
||||
archive, err := os.Open(o.archivePath)
|
||||
if err != nil {
|
||||
@@ -62,6 +77,8 @@ func (o *loadImageOperation) Apply(ctx context.Context) error {
|
||||
return o.engine.LoadImage(ctx, archive)
|
||||
}
|
||||
|
||||
// Inspect 检查加载操作是否已生效:镜像不存在时为未应用;存在时依据摘要与平台判定,
|
||||
// 匹配则返回已应用,否则返回未应用。结果携带镜像标识、仓库摘要、描述符摘要与平台信息。
|
||||
func (o *loadImageOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
image, err := o.engine.InspectImage(ctx, o.imageReference)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
@@ -86,17 +103,25 @@ func (o *loadImageOperation) Inspect(ctx context.Context) (transaction.Inspectio
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
}
|
||||
|
||||
// createContainerOperation 表示“创建非活动后端容器”的事务操作。
|
||||
type createContainerOperation struct {
|
||||
engine containerengine.Engine
|
||||
expectedImage containerengine.Image
|
||||
spec containerengine.ContainerSpec
|
||||
}
|
||||
|
||||
type removeContainerOperation struct {
|
||||
// engine 执行创建操作的容器引擎。
|
||||
engine containerengine.Engine
|
||||
name string
|
||||
// expectedImage 创建容器时期望使用的镜像。
|
||||
expectedImage containerengine.Image
|
||||
// spec 待创建容器的完整规格。
|
||||
spec containerengine.ContainerSpec
|
||||
}
|
||||
|
||||
// removeContainerOperation 表示“移除非活动后端容器”的事务操作。
|
||||
type removeContainerOperation struct {
|
||||
// engine 执行移除操作的容器引擎。
|
||||
engine containerengine.Engine
|
||||
// name 待移除容器的名称。
|
||||
name string
|
||||
}
|
||||
|
||||
// Apply 强制移除指定名称的非活动容器,容器不存在时视为成功。
|
||||
func (o *removeContainerOperation) Apply(ctx context.Context) error {
|
||||
err := o.engine.RemoveContainer(ctx, o.name, true)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
@@ -105,6 +130,7 @@ func (o *removeContainerOperation) Apply(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect 检查移除操作是否已生效:容器不存在即为已应用,存在则为未应用并返回容器信息。
|
||||
func (o *removeContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
record, err := o.engine.InspectContainer(ctx, o.name)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
@@ -116,11 +142,14 @@ func (o *removeContainerOperation) Inspect(ctx context.Context) (transaction.Ins
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: containerResult(record)}, nil
|
||||
}
|
||||
|
||||
// Apply 调用容器引擎按规格创建非活动后端容器。
|
||||
func (o *createContainerOperation) Apply(ctx context.Context) error {
|
||||
_, err := o.engine.CreateContainer(ctx, o.spec)
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect 检查创建操作是否已生效:容器不存在时为未应用;存在时若与期望镜像及规格完全匹配则为已应用,
|
||||
// 否则为未知状态(存在冲突),以便上层据此重放或报错。
|
||||
func (o *createContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
record, err := o.engine.InspectContainer(ctx, o.spec.Name)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
@@ -136,15 +165,21 @@ func (o *createContainerOperation) Inspect(ctx context.Context) (transaction.Ins
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
}
|
||||
|
||||
// startContainerOperation 表示“启动非活动后端容器”的事务操作。
|
||||
type startContainerOperation struct {
|
||||
// engine 执行启动操作的容器引擎。
|
||||
engine containerengine.Engine
|
||||
name string
|
||||
// name 待启动容器的名称。
|
||||
name string
|
||||
}
|
||||
|
||||
// Apply 调用容器引擎启动指定名称的后端容器。
|
||||
func (o *startContainerOperation) Apply(ctx context.Context) error {
|
||||
return o.engine.StartContainer(ctx, o.name)
|
||||
}
|
||||
|
||||
// Inspect 检查启动操作是否已生效:容器不存在时为未应用;存在且处于运行且非 Dead 状态时为已应用,
|
||||
// 否则为未应用。结果携带容器运行状态信息。
|
||||
func (o *startContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
record, err := o.engine.InspectContainer(ctx, o.name)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
@@ -160,18 +195,28 @@ func (o *startContainerOperation) Inspect(ctx context.Context) (transaction.Insp
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
||||
}
|
||||
|
||||
// healthOperation 表示“等待后端 Actuator 健康”的事务操作。
|
||||
type healthOperation struct {
|
||||
engine containerengine.Engine
|
||||
checker *healthcheck.ActuatorChecker
|
||||
name string
|
||||
// engine 用于查询容器运行状态。
|
||||
engine containerengine.Engine
|
||||
// checker Actuator 健康检查器。
|
||||
checker *healthcheck.ActuatorChecker
|
||||
// name 待检查容器的名称。
|
||||
name string
|
||||
// endpoint 健康检查的 HTTP 端点。
|
||||
endpoint string
|
||||
timeout time.Duration
|
||||
// timeout 等待健康的整体超时时间。
|
||||
timeout time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
// mu 保护 confirmed 与 confirmedReport 的并发访问。
|
||||
mu sync.Mutex
|
||||
// confirmedReport 已确认的健康报告。
|
||||
confirmedReport healthcheck.ActuatorReport
|
||||
confirmed bool
|
||||
// confirmed 表示健康检查结果是否已经确认。
|
||||
confirmed bool
|
||||
}
|
||||
|
||||
// Apply 调用健康检查器等待后端健康,成功后加锁保存已确认的健康报告并标记为已确认。
|
||||
func (o *healthOperation) Apply(ctx context.Context) error {
|
||||
report, err := o.checker.Wait(ctx, o.endpoint, o.timeout, o.running)
|
||||
if err != nil {
|
||||
@@ -184,6 +229,8 @@ func (o *healthOperation) Apply(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inspect 检查健康操作是否已生效:若先前已确认则直接返回已确认结果,
|
||||
// 否则执行一次即时健康检查并返回检查结论。
|
||||
func (o *healthOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
o.mu.Lock()
|
||||
if o.confirmed {
|
||||
@@ -196,6 +243,7 @@ func (o *healthOperation) Inspect(ctx context.Context) (transaction.Inspection,
|
||||
return healthInspection(report, ready, err)
|
||||
}
|
||||
|
||||
// running 查询后端容器是否处于运行且非 Dead 状态,容器不存在时返回 false 且无错误。
|
||||
func (o *healthOperation) running(ctx context.Context) (bool, error) {
|
||||
record, err := o.engine.InspectContainer(ctx, o.name)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
@@ -207,6 +255,8 @@ func (o *healthOperation) running(ctx context.Context) (bool, error) {
|
||||
return record.Running && !record.Dead, nil
|
||||
}
|
||||
|
||||
// healthInspection 将健康检查结果转换为事务检查结论:工作负载停止、检查出错或未就绪时均为未应用,
|
||||
// 仅当 ready 为 true 且无错误时才判定为已应用。结果始终携带健康报告的 JSON 序列化。
|
||||
func healthInspection(report healthcheck.ActuatorReport, ready bool, err error) (transaction.Inspection, error) {
|
||||
result := resultJSON(report)
|
||||
if errors.Is(err, healthcheck.ErrWorkloadStopped) {
|
||||
@@ -221,6 +271,8 @@ func healthInspection(report healthcheck.ActuatorReport, ready bool, err error)
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
}
|
||||
|
||||
// containerMatches 判断实际容器记录是否与期望镜像标识及期望规格完全匹配,
|
||||
// 包括镜像、网络模式、重启策略、用户、停止超时以及环境变量与挂载集合的逐项包含关系。
|
||||
func containerMatches(record containerengine.Container, expectedImageID string, spec containerengine.ContainerSpec) bool {
|
||||
if record.ImageID != expectedImageID || record.NetworkMode != spec.NetworkMode || record.RestartPolicy != spec.RestartPolicy || record.User != spec.User || record.StopTimeoutSeconds != spec.StopTimeoutSeconds {
|
||||
return false
|
||||
@@ -238,6 +290,7 @@ func containerMatches(record containerengine.Container, expectedImageID string,
|
||||
return true
|
||||
}
|
||||
|
||||
// containerResult 将容器记录的标识、镜像、运行、Dead 与状态字段序列化为 JSON RawMessage。
|
||||
func containerResult(record containerengine.Container) json.RawMessage {
|
||||
return resultJSON(struct {
|
||||
ID string `json:"id"`
|
||||
@@ -248,6 +301,7 @@ func containerResult(record containerengine.Container) json.RawMessage {
|
||||
}{record.ID, record.ImageID, record.Running, record.Dead, record.Status})
|
||||
}
|
||||
|
||||
// 以下编译期断言确保各操作类型均实现 transaction.Operation 接口。
|
||||
var _ transaction.Operation = (*loadImageOperation)(nil)
|
||||
var _ transaction.Operation = (*pullImageOperation)(nil)
|
||||
var _ transaction.Operation = (*removeContainerOperation)(nil)
|
||||
|
||||
Reference in New Issue
Block a user