feat: backend native executor implement

This commit is contained in:
2026-08-16 01:27:30 +08:00
parent fbce91d797
commit 390e0565d3
44 changed files with 6430 additions and 31 deletions
+393
View File
@@ -0,0 +1,393 @@
package backendupdate
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"yms-daemon/internal/filestore"
"yms-daemon/internal/hostnginx"
"yms-daemon/internal/systemd"
"yms-daemon/internal/transaction"
)
type gatewayOperation struct {
controller gatewayController
before hostnginx.Snapshot
after hostnginx.Snapshot
receiptPath string
}
func (o *gatewayOperation) Apply(ctx context.Context) error {
if err := o.controller.Apply(ctx, o.after); err != nil {
return err
}
return writeImmutableFile(o.receiptPath, []byte(snapshotDigest(o.after)), 0o600)
}
func (o *gatewayOperation) Inspect(context.Context) (transaction.Inspection, error) {
current, err := o.controller.Read()
if err != nil {
return transaction.Inspection{}, err
}
receipt, receiptErr := os.ReadFile(o.receiptPath)
if current.ActivePort == o.after.ActivePort && bytes.Equal(current.Content, o.after.Content) {
if receiptErr == nil && string(receipt) == snapshotDigest(o.after) {
return transaction.Inspection{Status: transaction.InspectionApplied, Result: gatewayResult(current)}, nil
}
if errors.Is(receiptErr, os.ErrNotExist) {
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: gatewayResult(current)}, nil
}
if receiptErr != nil {
return transaction.Inspection{}, fmt.Errorf("read host Nginx switch receipt: %w", receiptErr)
}
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: gatewayResult(current)}, nil
}
if current.ActivePort == o.before.ActivePort && bytes.Equal(current.Content, o.before.Content) {
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: gatewayResult(current)}, nil
}
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: gatewayResult(current)}, nil
}
type pathOperation struct {
path string
before pathState
desired pathState
}
func (o *pathOperation) Apply(context.Context) error {
return applyPathState(o.path, o.desired)
}
func (o *pathOperation) Inspect(context.Context) (transaction.Inspection, error) {
desired, err := pathMatches(o.path, o.desired)
if err != nil {
return transaction.Inspection{}, err
}
if desired {
return transaction.Inspection{Status: transaction.InspectionApplied, Result: pathResult(o.path, o.desired)}, nil
}
before, err := pathMatches(o.path, o.before)
if err != nil {
return transaction.Inspection{}, err
}
if before {
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: pathResult(o.path, o.before)}, nil
}
return transaction.Inspection{Status: transaction.InspectionUnknown}, 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, err := json.Marshal(unit)
if err != nil {
return transaction.Inspection{}, err
}
// systemd may leave a successfully stopped legacy service in failed when
// its tracked JVM exits with SIGTERM (status 143). Both states
// mean no process is active, which is the required result of this step.
if unit.ActiveState == "inactive" || unit.ActiveState == "failed" {
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
}
if unitRunning(unit) {
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
}
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: result}, nil
}
func snapshotPath(path string, backupPath string) (pathState, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return pathState{Kind: pathKindAbsent}, nil
}
if err != nil {
return pathState{}, fmt.Errorf("inspect compatibility path %s: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(path)
if err != nil {
return pathState{}, fmt.Errorf("read compatibility link %s: %w", path, err)
}
return pathState{Kind: pathKindSymlink, Target: target}, nil
}
if !info.Mode().IsRegular() {
return pathState{}, fmt.Errorf("compatibility path is neither a regular file nor symbolic link: %s", path)
}
identity, err := copyFileSnapshot(path, backupPath, info.Mode().Perm())
if err != nil {
return pathState{}, err
}
return pathState{Kind: pathKindRegular, BackupPath: backupPath, Identity: identity, Mode: uint32(info.Mode().Perm())}, nil
}
func applyPathState(path string, state pathState) error {
parent := filepath.Dir(path)
info, err := os.Lstat(parent)
if err != nil {
return fmt.Errorf("inspect path state parent %s: %w", parent, err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("path state parent is not a direct directory: %s", parent)
}
switch state.Kind {
case pathKindAbsent:
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove path state %s: %w", path, err)
}
return syncDirectory(parent)
case pathKindSymlink:
if !filepath.IsAbs(state.Target) {
return errors.New("path state symbolic link target must be absolute")
}
temporary := filepath.Join(parent, ".yms-daemon-link-"+rand.Text())
if err := os.Symlink(state.Target, temporary); err != nil {
return fmt.Errorf("create temporary compatibility link: %w", err)
}
defer os.Remove(temporary)
if err := os.Rename(temporary, path); err != nil {
return fmt.Errorf("replace compatibility link %s: %w", path, err)
}
return syncDirectory(parent)
case pathKindRegular:
if err := verifyFileIdentity(state.BackupPath, state.Identity); err != nil {
return fmt.Errorf("verify compatibility file snapshot: %w", err)
}
return copyFileAtomic(state.BackupPath, path, os.FileMode(state.Mode))
default:
return fmt.Errorf("unsupported path state kind: %q", state.Kind)
}
}
func pathMatches(path string, state pathState) (bool, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return state.Kind == pathKindAbsent, nil
}
if err != nil {
return false, err
}
switch state.Kind {
case pathKindAbsent:
return false, nil
case pathKindSymlink:
if info.Mode()&os.ModeSymlink == 0 {
return false, nil
}
target, err := os.Readlink(path)
return target == state.Target, err
case pathKindRegular:
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || uint32(info.Mode().Perm()) != state.Mode {
return false, nil
}
if err := verifyFileIdentity(path, state.Identity); err != nil {
return false, nil
}
return true, nil
default:
return false, fmt.Errorf("unsupported path state kind: %q", state.Kind)
}
}
func copyFileSnapshot(sourcePath string, destinationPath string, mode os.FileMode) (filestore.Identity, error) {
if err := os.MkdirAll(filepath.Dir(destinationPath), 0o750); err != nil {
return filestore.Identity{}, err
}
source, err := os.Open(sourcePath)
if err != nil {
return filestore.Identity{}, err
}
defer source.Close()
temporary, err := os.CreateTemp(filepath.Dir(destinationPath), ".snapshot-*")
if err != nil {
return filestore.Identity{}, err
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(mode); err != nil {
_ = temporary.Close()
return filestore.Identity{}, err
}
digest := sha256.New()
size, copyErr := io.Copy(io.MultiWriter(temporary, digest), source)
if copyErr != nil {
_ = temporary.Close()
return filestore.Identity{}, copyErr
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return filestore.Identity{}, err
}
if err := temporary.Close(); err != nil {
return filestore.Identity{}, err
}
if err := os.Rename(temporaryPath, destinationPath); err != nil {
return filestore.Identity{}, err
}
if err := syncDirectory(filepath.Dir(destinationPath)); err != nil {
return filestore.Identity{}, err
}
return filestore.Identity{Size: size, SHA256: hex.EncodeToString(digest.Sum(nil))}, nil
}
func copyFileAtomic(sourcePath string, destinationPath string, mode os.FileMode) error {
source, err := os.Open(sourcePath)
if err != nil {
return err
}
defer source.Close()
parent := filepath.Dir(destinationPath)
temporary, err := os.CreateTemp(parent, ".restore-*")
if err != nil {
return err
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(mode); err != nil {
_ = temporary.Close()
return err
}
if _, err := io.Copy(temporary, source); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := os.Rename(temporaryPath, destinationPath); err != nil {
return err
}
return syncDirectory(parent)
}
func writeImmutableFile(path string, content []byte, mode os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
if errors.Is(err, os.ErrExist) {
existing, readErr := os.ReadFile(path)
if readErr != nil {
return readErr
}
if !bytes.Equal(existing, content) {
return fmt.Errorf("immutable file already exists with different content: %s", path)
}
return nil
}
if err != nil {
return err
}
complete := false
defer func() {
if !complete {
_ = os.Remove(path)
}
}()
if _, err := file.Write(content); err != nil {
_ = file.Close()
return err
}
if err := file.Sync(); err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
if err := syncDirectory(filepath.Dir(path)); err != nil {
return err
}
complete = true
return nil
}
func readGatewaySnapshot(path string, port int) (hostnginx.Snapshot, error) {
content, err := os.ReadFile(path)
if err != nil {
return hostnginx.Snapshot{}, err
}
actual, err := hostnginx.ActiveBackendPort(content)
if err != nil {
return hostnginx.Snapshot{}, err
}
if actual != port {
return hostnginx.Snapshot{}, fmt.Errorf("persisted host Nginx snapshot port mismatch: got %d, want %d", actual, port)
}
return hostnginx.Snapshot{Content: content, ActivePort: port}, nil
}
func verifyFileIdentity(path string, identity filestore.Identity) error {
if err := identity.Validate(); err != nil {
return err
}
file, err := os.Open(path)
if err != nil {
return err
}
digest := sha256.New()
size, copyErr := io.Copy(digest, file)
closeErr := file.Close()
if err := errors.Join(copyErr, closeErr); err != nil {
return err
}
if size != identity.Size || !strings.EqualFold(hex.EncodeToString(digest.Sum(nil)), identity.SHA256) {
return errors.New("file identity mismatch")
}
return nil
}
func snapshotDigest(snapshot hostnginx.Snapshot) string {
digest := sha256.Sum256(snapshot.Content)
return hex.EncodeToString(digest[:])
}
func gatewayResult(snapshot hostnginx.Snapshot) json.RawMessage {
result, _ := json.Marshal(struct {
ActivePort int `json:"activePort"`
SHA256 string `json:"sha256"`
}{snapshot.ActivePort, snapshotDigest(snapshot)})
return result
}
func pathResult(path string, state pathState) json.RawMessage {
result, _ := json.Marshal(struct {
Path string `json:"path"`
State pathState `json:"state"`
}{path, state})
return result
}
func syncDirectory(directory string) error {
file, err := os.Open(directory)
if err != nil {
return err
}
syncErr := file.Sync()
closeErr := file.Close()
return errors.Join(syncErr, closeErr)
}