Files
orbit/cmd/orbit-admin/retention.go

109 lines
4.4 KiB
Go

package main
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/config"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/retention"
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
core "git.yuntuo.hk/k2safe/orbit/model"
)
func runRetentionArchive(args []string) error {
flags := flag.NewFlagSet("retention archive", flag.ContinueOnError)
var spec retention.JobSpec
var directory, namespace string
var options retention.JobOptions
var timeout time.Duration
flags.StringVar(&directory, "directory", os.Getenv("ORBIT_ARCHIVE_DIRECTORY"), "private per-job output directory; same directory resumes saved batches")
flags.StringVar(&namespace, "namespace", "", "namespace ID to archive")
flags.StringVar(&spec.Kind, "kind", "", "tasks|workflows|events|audit|deliveries|inbox|idempotency")
flags.Uint64Var(&spec.PolicyRevision, "revision", 0, "confirmed database retention policy revision")
flags.IntVar(&spec.BatchSize, "batch-size", 20, "roots per batch, 1-100; immutable for this job")
flags.IntVar(&options.MaxBatches, "max-batches", 10, "maximum new batches per invocation, 1-1000")
flags.Int64Var(&options.MaxBytes, "max-bytes", 256<<20, "total saved manifest and batch bytes, 1 MiB-100 GiB")
flags.DurationVar(&timeout, "timeout", 5*time.Minute, "invocation deadline, 1s-1h")
if err := flags.Parse(args); err != nil {
return err
}
spec.NamespaceID = core.ID(namespace)
if flags.NArg() != 0 || directory == "" || !filepath.IsAbs(directory) || timeout < time.Second || timeout > time.Hour || options.MaxBatches < 1 || options.MaxBatches > 1000 || options.MaxBytes < 1<<20 || options.MaxBytes > 100<<30 {
return errors.New("invalid archive arguments; use --help for limits and an absolute --directory")
}
if err := spec.Validate(); err != nil {
return err
}
cfg, err := config.Load()
if err != nil {
return err
}
signalCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ctx, cancel := context.WithTimeout(signalCtx, timeout)
defer cancel()
db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1)
if err != nil {
return errors.New("database unavailable")
}
defer db.Close()
if err := mysql.CheckSchema(ctx, db); err != nil {
return errors.New("database schema is not ready")
}
store := mysql.NewRetentionPolicyStore(db, cfg.RetentionDefaults)
status, runErr := retention.RunArchive(ctx, directory, spec, options, func(ctx context.Context, job core.ID, input retention.ArchiveInput) (retention.ArchiveSnapshot, error) {
snapshot, err := store.ExportRetentionForJob(ctx, spec.NamespaceID, job, input)
if err != nil {
// 数据库原始错误可能含 SQL 参数,只输出固定错误码,业务正文不进入控制台日志。
if ctx.Err() != nil {
return retention.ArchiveSnapshot{}, ctx.Err()
}
return retention.ArchiveSnapshot{}, fmt.Errorf("archive database export failed (%s); keep original directory and verify policy before retrying", fault.Code(err))
}
return snapshot, nil
})
if err := json.NewEncoder(os.Stdout).Encode(status); err != nil {
return err
}
return runErr
}
func runRetentionVerifyArchive(args []string) error {
flags := flag.NewFlagSet("retention verify-archive", flag.ContinueOnError)
var directory, expectedSHA string
flags.StringVar(&directory, "directory", os.Getenv("ORBIT_ARCHIVE_DIRECTORY"), "saved archive job directory; no database connection required")
flags.StringVar(&expectedSHA, "expected-sha256", "", "optional independently saved last_sha256 to pin the entire chain")
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() != 0 || directory == "" {
return errors.New("--directory is required")
}
if expectedSHA != "" {
raw, err := hex.DecodeString(expectedSHA)
if err != nil || len(raw) != 32 || hex.EncodeToString(raw) != expectedSHA {
return errors.New("--expected-sha256 must be 64 lowercase hexadecimal characters")
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
status, err := retention.VerifyArchive(ctx, directory)
if encodeErr := json.NewEncoder(os.Stdout).Encode(status); encodeErr != nil {
return encodeErr
}
if err == nil && expectedSHA != "" && status.LastSHA256 != expectedSHA {
return errors.New("archive differs from independently saved digest")
}
return err
}