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)