feat: backend native executor implement
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
package nativebackendexecutor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"yms-daemon/internal/filestore"
|
||||
"yms-daemon/internal/healthcheck"
|
||||
"yms-daemon/internal/systemd"
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
type installJarOperation struct {
|
||||
store *filestore.Store
|
||||
sourcePath string
|
||||
releasePath string
|
||||
identity filestore.Identity
|
||||
}
|
||||
|
||||
func (o *installJarOperation) Apply(context.Context) error {
|
||||
source, err := os.Open(o.sourcePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open native backend JAR: %w", err)
|
||||
}
|
||||
defer source.Close()
|
||||
_, err = o.store.Commit(o.releasePath, source, o.identity)
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *installJarOperation) Inspect(context.Context) (transaction.Inspection, error) {
|
||||
file, found, err := o.store.Inspect(o.releasePath, o.identity)
|
||||
if errors.Is(err, filestore.ErrDestinationConflict) {
|
||||
return transaction.Inspection{Status: transaction.InspectionUnknown}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
if !found {
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: resultJSON(file)}, nil
|
||||
}
|
||||
|
||||
type slotLinkOperation struct {
|
||||
path string
|
||||
desiredTarget string
|
||||
previousTarget string
|
||||
}
|
||||
|
||||
func (o *slotLinkOperation) Apply(ctx context.Context) error {
|
||||
inspection, err := o.Inspect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch inspection.Status {
|
||||
case transaction.InspectionApplied:
|
||||
return nil
|
||||
case transaction.InspectionNotApplied:
|
||||
case transaction.InspectionUnknown:
|
||||
return fmt.Errorf("native backend slot link %s does not match the recorded previous target", o.path)
|
||||
default:
|
||||
return fmt.Errorf("native backend slot link %s returned invalid inspection status %q", o.path, inspection.Status)
|
||||
}
|
||||
|
||||
parent := filepath.Dir(o.path)
|
||||
parentInfo, err := os.Lstat(parent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect native backend slot directory %s: %w", parent, err)
|
||||
}
|
||||
if !parentInfo.IsDir() || parentInfo.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("native backend slot parent is not a direct directory: %s", parent)
|
||||
}
|
||||
if o.desiredTarget == "" {
|
||||
if err := os.Remove(o.path); err != nil {
|
||||
return fmt.Errorf("remove native backend slot link %s: %w", o.path, err)
|
||||
}
|
||||
return syncDirectory(parent)
|
||||
}
|
||||
targetInfo, err := os.Lstat(o.desiredTarget)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect native backend slot target %s: %w", o.desiredTarget, err)
|
||||
}
|
||||
if !targetInfo.Mode().IsRegular() || targetInfo.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("native backend slot target is not a regular file: %s", o.desiredTarget)
|
||||
}
|
||||
|
||||
temporary, err := os.CreateTemp(parent, ".slot-link-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reserve native backend slot link path: %w", err)
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
if err := temporary.Close(); err != nil {
|
||||
_ = os.Remove(temporaryPath)
|
||||
return fmt.Errorf("close native backend slot link reservation: %w", err)
|
||||
}
|
||||
if err := os.Remove(temporaryPath); err != nil {
|
||||
return fmt.Errorf("remove native backend slot link reservation: %w", err)
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = os.Remove(temporaryPath)
|
||||
}
|
||||
}()
|
||||
if err := os.Symlink(o.desiredTarget, temporaryPath); err != nil {
|
||||
return fmt.Errorf("create native backend slot link: %w", err)
|
||||
}
|
||||
if err := os.Rename(temporaryPath, o.path); err != nil {
|
||||
return fmt.Errorf("replace native backend slot link %s: %w", o.path, err)
|
||||
}
|
||||
committed = true
|
||||
return syncDirectory(parent)
|
||||
}
|
||||
|
||||
func (o *slotLinkOperation) Inspect(context.Context) (transaction.Inspection, error) {
|
||||
info, err := os.Lstat(o.path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if o.desiredTarget == "" {
|
||||
if err := syncDirectory(filepath.Dir(o.path)); err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: linkResult(o.path, "")}, nil
|
||||
}
|
||||
if o.previousTarget == "" {
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionUnknown}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
return transaction.Inspection{Status: transaction.InspectionUnknown}, nil
|
||||
}
|
||||
target, err := os.Readlink(o.path)
|
||||
if err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
result := linkResult(o.path, target)
|
||||
if target == o.desiredTarget {
|
||||
if err := syncDirectory(filepath.Dir(o.path)); err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
}
|
||||
if target == o.previousTarget {
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: result}, nil
|
||||
}
|
||||
|
||||
type unitStartOperation struct {
|
||||
units systemd.Manager
|
||||
name string
|
||||
}
|
||||
|
||||
func (o *unitStartOperation) Apply(ctx context.Context) error {
|
||||
return o.units.Start(ctx, o.name)
|
||||
}
|
||||
|
||||
func (o *unitStartOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
unit, err := o.units.Inspect(ctx, o.name)
|
||||
if err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
result := unitResult(unit)
|
||||
switch unit.ActiveState {
|
||||
case activeState:
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
case inactiveState, failedState:
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
||||
default:
|
||||
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: result}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type unitStopOperation struct {
|
||||
units systemd.Manager
|
||||
name string
|
||||
}
|
||||
|
||||
func (o *unitStopOperation) Apply(ctx context.Context) error {
|
||||
return o.units.Stop(ctx, o.name)
|
||||
}
|
||||
|
||||
func (o *unitStopOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
unit, err := o.units.Inspect(ctx, o.name)
|
||||
if err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
result := unitResult(unit)
|
||||
switch unit.ActiveState {
|
||||
case inactiveState, failedState:
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
case activeState:
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
||||
default:
|
||||
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: result}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type healthOperation struct {
|
||||
units systemd.Manager
|
||||
checker actuatorChecker
|
||||
unitName 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) {
|
||||
unit, err := o.units.Inspect(ctx, o.unitName)
|
||||
if errors.Is(err, systemd.ErrUnitNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return unit.ActiveState == activeState, 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 || !ready {
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
}
|
||||
|
||||
func linkResult(path, target string) json.RawMessage {
|
||||
return resultJSON(struct {
|
||||
Path string `json:"path"`
|
||||
Target string `json:"target"`
|
||||
}{path, target})
|
||||
}
|
||||
|
||||
func unitResult(unit systemd.Unit) json.RawMessage {
|
||||
return resultJSON(struct {
|
||||
Name string `json:"name"`
|
||||
LoadState string `json:"loadState"`
|
||||
ActiveState string `json:"activeState"`
|
||||
SubState string `json:"subState"`
|
||||
}{unit.Name, unit.LoadState, unit.ActiveState, unit.SubState})
|
||||
}
|
||||
|
||||
func resultJSON(value any) json.RawMessage {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("marshal internal step result: %v", err))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func syncDirectory(path string) error {
|
||||
directory, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open directory for flush: %w", err)
|
||||
}
|
||||
syncErr := directory.Sync()
|
||||
closeErr := directory.Close()
|
||||
if err := errors.Join(syncErr, closeErr); err != nil {
|
||||
return fmt.Errorf("flush directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ transaction.Operation = (*installJarOperation)(nil)
|
||||
var _ transaction.Operation = (*slotLinkOperation)(nil)
|
||||
var _ transaction.Operation = (*unitStartOperation)(nil)
|
||||
var _ transaction.Operation = (*unitStopOperation)(nil)
|
||||
var _ transaction.Operation = (*healthOperation)(nil)
|
||||
Reference in New Issue
Block a user