2026-08-15 20:58:01 +08:00
|
|
|
package backendexecutor
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"slices"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"yms-daemon/internal/containerengine"
|
|
|
|
|
"yms-daemon/internal/healthcheck"
|
|
|
|
|
"yms-daemon/internal/transaction"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type loadImageOperation struct {
|
|
|
|
|
engine containerengine.Engine
|
|
|
|
|
archivePath string
|
|
|
|
|
imageReference string
|
|
|
|
|
expectedDigest string
|
|
|
|
|
platform containerengine.Platform
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 17:12:06 +08:00
|
|
|
type pullImageOperation struct {
|
|
|
|
|
engine containerengine.Engine
|
|
|
|
|
imageReference string
|
|
|
|
|
expectedDigest string
|
|
|
|
|
platform containerengine.Platform
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *pullImageOperation) Apply(ctx context.Context) error {
|
|
|
|
|
return o.engine.PullImage(ctx, o.imageReference)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *pullImageOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
|
|
|
|
image, err := o.engine.InspectImage(ctx, o.imageReference)
|
|
|
|
|
if errors.Is(err, containerengine.ErrNotFound) {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{}, err
|
|
|
|
|
}
|
|
|
|
|
matches, err := imageMatches(image, o.expectedDigest, o.platform)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{}, err
|
|
|
|
|
}
|
|
|
|
|
result := resultJSON(image)
|
|
|
|
|
if !matches {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 20:58:01 +08:00
|
|
|
func (o *loadImageOperation) Apply(ctx context.Context) error {
|
|
|
|
|
archive, err := os.Open(o.archivePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("open image archive: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer archive.Close()
|
|
|
|
|
return o.engine.LoadImage(ctx, archive)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *loadImageOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
|
|
|
|
image, err := o.engine.InspectImage(ctx, o.imageReference)
|
|
|
|
|
if errors.Is(err, containerengine.ErrNotFound) {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{}, err
|
|
|
|
|
}
|
|
|
|
|
matches, err := imageMatches(image, o.expectedDigest, o.platform)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{}, err
|
|
|
|
|
}
|
|
|
|
|
result := resultJSON(struct {
|
|
|
|
|
ImageID string `json:"imageId"`
|
|
|
|
|
RepoDigests []string `json:"repoDigests"`
|
|
|
|
|
DescriptorDigest string `json:"descriptorDigest"`
|
|
|
|
|
Platform containerengine.Platform `json:"platform"`
|
|
|
|
|
}{image.ID, image.RepoDigests, image.DescriptorDigest, image.Platform})
|
|
|
|
|
if !matches {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type createContainerOperation struct {
|
|
|
|
|
engine containerengine.Engine
|
|
|
|
|
expectedImage containerengine.Image
|
|
|
|
|
spec containerengine.ContainerSpec
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 17:12:06 +08:00
|
|
|
type removeContainerOperation struct {
|
|
|
|
|
engine containerengine.Engine
|
|
|
|
|
name string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *removeContainerOperation) Apply(ctx context.Context) error {
|
|
|
|
|
err := o.engine.RemoveContainer(ctx, o.name, true)
|
|
|
|
|
if errors.Is(err, containerengine.ErrNotFound) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *removeContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
|
|
|
|
record, err := o.engine.InspectContainer(ctx, o.name)
|
|
|
|
|
if errors.Is(err, containerengine.ErrNotFound) {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionApplied}, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{}, err
|
|
|
|
|
}
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: containerResult(record)}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 20:58:01 +08:00
|
|
|
func (o *createContainerOperation) Apply(ctx context.Context) error {
|
|
|
|
|
_, err := o.engine.CreateContainer(ctx, o.spec)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{}, err
|
|
|
|
|
}
|
|
|
|
|
result := containerResult(record)
|
|
|
|
|
if !containerMatches(record, o.expectedImage.ID, o.spec) {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type startContainerOperation struct {
|
|
|
|
|
engine containerengine.Engine
|
|
|
|
|
name string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *startContainerOperation) Apply(ctx context.Context) error {
|
|
|
|
|
return o.engine.StartContainer(ctx, o.name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *startContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
|
|
|
|
record, err := o.engine.InspectContainer(ctx, o.name)
|
|
|
|
|
if errors.Is(err, containerengine.ErrNotFound) {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{}, err
|
|
|
|
|
}
|
|
|
|
|
result := containerResult(record)
|
|
|
|
|
if record.Running && !record.Dead {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type healthOperation struct {
|
|
|
|
|
engine containerengine.Engine
|
|
|
|
|
checker *healthcheck.ActuatorChecker
|
|
|
|
|
name string
|
|
|
|
|
endpoint string
|
|
|
|
|
timeout time.Duration
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
confirmedReport healthcheck.ActuatorReport
|
|
|
|
|
confirmed bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *healthOperation) Apply(ctx context.Context) error {
|
|
|
|
|
report, err := o.checker.Wait(ctx, o.endpoint, o.timeout, o.running)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
o.mu.Lock()
|
|
|
|
|
o.confirmedReport = report
|
|
|
|
|
o.confirmed = true
|
|
|
|
|
o.mu.Unlock()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *healthOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
|
|
|
|
o.mu.Lock()
|
|
|
|
|
if o.confirmed {
|
|
|
|
|
report := o.confirmedReport
|
|
|
|
|
o.mu.Unlock()
|
|
|
|
|
return healthInspection(report, true, nil)
|
|
|
|
|
}
|
|
|
|
|
o.mu.Unlock()
|
|
|
|
|
report, ready, err := o.checker.Check(ctx, o.endpoint, o.running)
|
|
|
|
|
return healthInspection(report, ready, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *healthOperation) running(ctx context.Context) (bool, error) {
|
|
|
|
|
record, err := o.engine.InspectContainer(ctx, o.name)
|
|
|
|
|
if errors.Is(err, containerengine.ErrNotFound) {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
return record.Running && !record.Dead, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func healthInspection(report healthcheck.ActuatorReport, ready bool, err error) (transaction.Inspection, error) {
|
|
|
|
|
result := resultJSON(report)
|
|
|
|
|
if errors.Is(err, healthcheck.ErrWorkloadStopped) {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
if !ready {
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerMatches(record containerengine.Container, expectedImageID string, spec containerengine.ContainerSpec) bool {
|
2026-08-16 17:12:06 +08:00
|
|
|
if record.ImageID != expectedImageID || record.NetworkMode != spec.NetworkMode || record.RestartPolicy != spec.RestartPolicy || record.User != spec.User || record.StopTimeoutSeconds != spec.StopTimeoutSeconds {
|
2026-08-15 20:58:01 +08:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, expected := range spec.Environment {
|
|
|
|
|
if !slices.Contains(record.Environment, expected) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, expected := range spec.Mounts {
|
|
|
|
|
if !slices.Contains(record.Mounts, expected) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerResult(record containerengine.Container) json.RawMessage {
|
|
|
|
|
return resultJSON(struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
ImageID string `json:"imageId"`
|
|
|
|
|
Running bool `json:"running"`
|
|
|
|
|
Dead bool `json:"dead"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
}{record.ID, record.ImageID, record.Running, record.Dead, record.Status})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ transaction.Operation = (*loadImageOperation)(nil)
|
2026-08-16 17:12:06 +08:00
|
|
|
var _ transaction.Operation = (*pullImageOperation)(nil)
|
|
|
|
|
var _ transaction.Operation = (*removeContainerOperation)(nil)
|
2026-08-15 20:58:01 +08:00
|
|
|
var _ transaction.Operation = (*createContainerOperation)(nil)
|
|
|
|
|
var _ transaction.Operation = (*startContainerOperation)(nil)
|
|
|
|
|
var _ transaction.Operation = (*healthOperation)(nil)
|