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 } 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 } 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 } 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 } 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 { if record.ImageID != expectedImageID || record.NetworkMode != spec.NetworkMode || record.RestartPolicy != spec.RestartPolicy || record.User != spec.User || record.StopTimeoutSeconds != spec.StopTimeoutSeconds { 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) var _ transaction.Operation = (*pullImageOperation)(nil) var _ transaction.Operation = (*removeContainerOperation)(nil) var _ transaction.Operation = (*createContainerOperation)(nil) var _ transaction.Operation = (*startContainerOperation)(nil) var _ transaction.Operation = (*healthOperation)(nil)