feat: backend executor implement

This commit is contained in:
2026-08-16 17:12:06 +08:00
parent 390e0565d3
commit 7755da72e7
31 changed files with 2762 additions and 184 deletions
+109 -41
View File
@@ -24,31 +24,44 @@ import (
)
const (
healthPath = "/yms/actuator/health"
healthTimeout = 120 * time.Second
healthInterval = time.Second
hostNetworkMode = "host"
bindMountType = "bind"
stepLoadImage = "backend.image.load"
stepCreateContainer = "backend.container.create"
stepStartContainer = "backend.container.start"
stepCheckHealth = "backend.container.health"
healthPath = "/yms/actuator/health"
healthTimeout = 120 * time.Second
healthInterval = time.Second
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"
)
const (
ImageAcquisitionLoad = "load"
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.
type Request struct {
ArchivePath string
ImageReference string
ExpectedImageDigest string
Platform containerengine.Platform
ContainerName string
Port int
PortEnvironmentKey string
ConfigSource string
ConfigTarget string
RestartPolicy containerengine.RestartPolicy
HealthEndpoint string
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
ConfigEnvironmentKey string
ConfigLocation string
RestartPolicy containerengine.RestartPolicy
HealthEndpoint string
}
// Executor drives the persisted transaction up to SWITCHING after the new container is healthy.
@@ -115,10 +128,10 @@ func (e *Executor) run(ctx context.Context, transactionID string, request Reques
case transaction.StateStarting:
// 重放 PREPARED 步骤会核对持久化意图,阻止恢复时换入另一组请求参数。
if err := e.prepare(ctx, transactionID, request); err != nil {
return e.failUnlessRecoverable(ctx, transactionID, err)
return err
}
if err := e.startAndCheck(ctx, transactionID, request); err != nil {
return e.failUnlessRecoverable(ctx, transactionID, err)
return err
}
if _, err := e.store.Transition(ctx, transactionID, transaction.StateSwitching, "backend container is healthy"); err != nil {
return err
@@ -144,12 +157,17 @@ func (e *Executor) validate(ctx context.Context, request Request) error {
if err := validateRequest(request); err != nil {
return err
}
if err := regularFile(request.ArchivePath, "image archive"); err != nil {
return err
if request.ImageAcquisition == ImageAcquisitionLoad {
if err := regularFile(request.ArchivePath, "image archive"); err != nil {
return err
}
}
if err := regularFile(request.ConfigSource, "backend configuration"); err != nil {
return err
}
if err := directDirectory(request.TmpSource, "backend temporary directory"); err != nil {
return err
}
if err := e.engine.Ping(ctx); err != nil {
return err
}
@@ -157,21 +175,27 @@ func (e *Executor) validate(ctx context.Context, request Request) error {
}
func (e *Executor) prepare(ctx context.Context, transactionID string, request Request) error {
loadOperation := &loadImageOperation{
engine: e.engine,
archivePath: request.ArchivePath,
imageReference: request.ImageReference,
expectedDigest: request.ExpectedImageDigest,
platform: request.Platform,
}
if _, err := e.coordinator.ExecuteStep(ctx, transactionID, loadIntent(request), loadOperation); err != nil {
return err
switch request.ImageAcquisition {
case ImageAcquisitionLoad:
operation := &loadImageOperation{engine: e.engine, archivePath: request.ArchivePath, imageReference: request.ImageReference, expectedDigest: request.ExpectedImageDigest, platform: request.Platform}
if _, err := e.coordinator.ExecuteStep(ctx, transactionID, loadIntent(request), operation); err != nil {
return err
}
case ImageAcquisitionPull:
operation := &pullImageOperation{engine: e.engine, imageReference: request.ImageReference, expectedDigest: request.ExpectedImageDigest, platform: request.Platform}
if _, err := e.coordinator.ExecuteStep(ctx, transactionID, pullIntent(request), operation); err != nil {
return err
}
}
image, err := e.engine.InspectImage(ctx, request.ImageReference)
if err != nil {
return fmt.Errorf("inspect prepared backend image: %w", err)
}
removeOperation := &removeContainerOperation{engine: e.engine, name: request.ContainerName}
if _, err := e.coordinator.ExecuteStep(ctx, transactionID, removeIntent(request), removeOperation); err != nil {
return err
}
createOperation := &createContainerOperation{
engine: e.engine,
expectedImage: image,
@@ -198,8 +222,17 @@ func (e *Executor) startAndCheck(ctx context.Context, transactionID string, requ
}
func validateRequest(request Request) error {
if !filepath.IsAbs(request.ArchivePath) {
return errors.New("image archive path must be absolute")
switch request.ImageAcquisition {
case ImageAcquisitionLoad:
if !filepath.IsAbs(request.ArchivePath) {
return errors.New("image archive path must be absolute")
}
case ImageAcquisitionPull:
if request.ArchivePath != "" {
return errors.New("pull image acquisition does not accept an archive path")
}
default:
return fmt.Errorf("unsupported image acquisition: %q", request.ImageAcquisition)
}
if request.ImageReference == "" || strings.TrimSpace(request.ImageReference) != request.ImageReference {
return errors.New("exact image reference is required")
@@ -222,6 +255,15 @@ func validateRequest(request Request) error {
if !filepath.IsAbs(request.ConfigSource) || !filepath.IsAbs(request.ConfigTarget) {
return errors.New("backend configuration source and target must be absolute paths")
}
if !filepath.IsAbs(request.TmpSource) || !filepath.IsAbs(request.TmpTarget) {
return errors.New("backend temporary source and target must be absolute paths")
}
if request.ConfigEnvironmentKey == "" || strings.Contains(request.ConfigEnvironmentKey, "=") || strings.TrimSpace(request.ConfigEnvironmentKey) != request.ConfigEnvironmentKey {
return errors.New("exact backend configuration environment key is required")
}
if request.ConfigLocation == "" || strings.TrimSpace(request.ConfigLocation) != request.ConfigLocation {
return errors.New("exact backend configuration location is required")
}
if err := validateRestartPolicy(request.RestartPolicy); err != nil {
return err
}
@@ -262,6 +304,17 @@ func regularFile(path, description string) error {
return nil
}
func directDirectory(path, description string) error {
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("inspect %s %s: %w", description, path, err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("%s is not a direct directory: %s", description, path)
}
return nil
}
func containerSpec(request Request) containerengine.ContainerSpec {
return containerengine.ContainerSpec{
Name: request.ContainerName,
@@ -269,18 +322,27 @@ func containerSpec(request Request) containerengine.ContainerSpec {
Platform: request.Platform,
Environment: []string{
request.PortEnvironmentKey + "=" + strconv.Itoa(request.Port),
request.ConfigEnvironmentKey + "=" + request.ConfigLocation,
},
NetworkMode: hostNetworkMode,
RestartPolicy: request.RestartPolicy,
Mounts: []containerengine.Mount{{
Type: bindMountType,
Source: request.ConfigSource,
Target: request.ConfigTarget,
ReadOnly: true,
}},
Mounts: []containerengine.Mount{
{Type: bindMountType, Source: request.ConfigSource, Target: request.ConfigTarget, ReadOnly: true},
{Type: bindMountType, Source: request.TmpSource, Target: request.TmpTarget},
},
User: "0:0",
StopTimeoutSeconds: containerStopTimeoutSeconds,
}
}
func pullIntent(request Request) transaction.StepIntent {
return intent(stepPullImage, "pull and verify backend image", struct {
ImageReference string `json:"imageReference"`
ImageDigest string `json:"imageDigest"`
Platform containerengine.Platform `json:"platform"`
}{request.ImageReference, request.ExpectedImageDigest, request.Platform})
}
func loadIntent(request Request) transaction.StepIntent {
return intent(stepLoadImage, "load and verify backend image", struct {
ArchivePath string `json:"archivePath"`
@@ -297,6 +359,12 @@ func createIntent(request Request, imageID string) transaction.StepIntent {
}{containerSpec(request), imageID})
}
func removeIntent(request Request) transaction.StepIntent {
return intent(stepRemoveContainer, "remove inactive backend container", struct {
ContainerName string `json:"containerName"`
}{request.ContainerName})
}
func startIntent(request Request) transaction.StepIntent {
return intent(stepStartContainer, "start inactive backend container", struct {
ContainerName string `json:"containerName"`