109 lines
4.4 KiB
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
|
|
}
|