package main import ( "context" "encoding/json" "errors" "flag" "fmt" "io" "os" "path/filepath" "reflect" "time" "git.yuntuo.hk/k2safe/orbit/internal/buildinfo" "git.yuntuo.hk/k2safe/orbit/internal/config" "git.yuntuo.hk/k2safe/orbit/internal/storage/mysql" core "git.yuntuo.hk/k2safe/orbit/model" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run() error { if len(os.Args) < 2 { return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply") } switch os.Args[1] { case "release": return runRelease(os.Args[2:]) case "bootstrap": return runBootstrap(os.Args[2:]) case "verify": return runVerify(os.Args[2:]) case "observe": return runObserve(os.Args[2:]) case "retention": return runRetention(os.Args[2:]) case "credential": return runCredential(os.Args[2:]) } return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply") } // releaseManifest 是发布物自身的静态身份清单,不读取数据库、不包含 DSN、凭据或业务数据。 // 部署前可以保存它,与镜像摘要、git 提交和目标数据库的 verify 输出一起作为发布证据。 type releaseManifest struct { ManifestVersion int `json:"manifest_version"` // ManifestVersion 清单格式版本,变更时递增。 Build buildinfo.Info `json:"build"` // Build 当前二进制及协议身份。 Schema releaseSchemaManifest `json:"schema"` // Schema 当前内置迁移链摘要。 } // releaseSchemaManifest 描述当前二进制可执行的前向迁移链;SQL 正文不写入清单。 type releaseSchemaManifest struct { CurrentVersion uint64 `json:"current_version"` // CurrentVersion 最新迁移版本。 MigrationCount int `json:"migration_count"` // MigrationCount 迁移数量。 Migrations []releaseMigrationManifest `json:"migrations"` // Migrations 按版本排序的名称和校验和。 } // releaseMigrationManifest 是单条迁移的不可变身份,不暴露迁移 SQL。 type releaseMigrationManifest struct { Version uint64 `json:"version"` // Version 迁移版本。 Name string `json:"name"` // Name 迁移文件名。 Checksum string `json:"checksum"` // Checksum 迁移内容 SHA-256。 } func runRelease(args []string) error { if len(args) != 1 || args[0] != "manifest" { return errors.New("usage: orbit-admin release manifest") } manifest, err := currentReleaseManifest() if err != nil { return err } return json.NewEncoder(os.Stdout).Encode(manifest) } func currentReleaseManifest() (releaseManifest, error) { migrations, err := mysql.Migrations() if err != nil || len(migrations) == 0 { return releaseManifest{}, errors.New("release migration manifest unavailable") } items := make([]releaseMigrationManifest, 0, len(migrations)) for _, migration := range migrations { items = append(items, releaseMigrationManifest{ Version: migration.Version, Name: migration.Name, Checksum: migration.Checksum, }) } return releaseManifest{ ManifestVersion: 1, Build: buildinfo.Current(), Schema: releaseSchemaManifest{ CurrentVersion: migrations[len(migrations)-1].Version, MigrationCount: len(items), Migrations: items, }, }, nil } // validateReleaseManifest 确认外部保存的发布清单与当前管理二进制完全一致。 // 它只比较静态身份,不连接数据库,也不改变任何状态。 func validateReleaseManifest(path string) error { file, err := os.Open(path) if err != nil { return errors.New("release manifest could not be opened") } defer file.Close() decoder := json.NewDecoder(file) decoder.DisallowUnknownFields() var got releaseManifest if err := decoder.Decode(&got); err != nil { return errors.New("release manifest is invalid") } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { return errors.New("release manifest contains trailing data") } want, err := currentReleaseManifest() if err != nil { return err } if !reflect.DeepEqual(got, want) { return errors.New("release manifest does not match this binary") } return nil } func runVerify(args []string) error { flags := flag.NewFlagSet("verify", flag.ContinueOnError) var manifestPath string flags.StringVar(&manifestPath, "manifest", "", "saved release manifest to compare with this binary") if err := flags.Parse(args); err != nil { return err } if flags.NArg() != 0 { return errors.New("verify does not accept positional arguments") } if manifestPath != "" { if err := validateReleaseManifest(manifestPath); err != nil { return err } } cfg, err := config.Load() if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1) if err != nil { return errors.New("database unavailable") } defer db.Close() verification, err := mysql.VerifyOperationalSchema(ctx, db) encoded, encodeErr := json.Marshal(verification) if encodeErr != nil { return encodeErr } fmt.Println(string(encoded)) if err != nil { return err } return nil } // runObserve exposes the same bounded, read-only snapshot used by the private // metrics sampler. It is deliberately an operator command rather than a // tenant-facing API: values are per database node and may be truncated. func runObserve(args []string) error { if len(args) != 1 || args[0] != "snapshot" { return errors.New("usage: orbit-admin observe snapshot") } cfg, err := config.Load() if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1) if err != nil { return errors.New("database unavailable") } defer db.Close() snapshot, err := mysql.ReadOperationalSnapshot(ctx, db) if err != nil { return errors.New("operational snapshot unavailable") } output := struct { Snapshot mysql.OperationalSnapshot `json:"snapshot"` SampleLimit int `json:"sample_limit"` EventWindowSeconds int `json:"event_window_seconds"` Scope string `json:"scope"` }{ Snapshot: snapshot, SampleLimit: 10000, EventWindowSeconds: 300, Scope: "single database node; bounded sample, not a global exact count", } return json.NewEncoder(os.Stdout).Encode(output) } func runRetention(args []string) error { if len(args) == 0 || args[0] != "inventory" { return errors.New("usage: orbit-admin retention inventory --before RFC3339") } flags := flag.NewFlagSet("retention inventory", flag.ContinueOnError) var before string flags.StringVar(&before, "before", "", "UTC cutoff; only records strictly before it are counted") if err := flags.Parse(args[1:]); err != nil { return err } if flags.NArg() != 0 || before == "" { return errors.New("--before RFC3339 is required") } cutoff, err := time.Parse(time.RFC3339Nano, before) if err != nil { return errors.New("--before must be RFC3339") } cfg, err := config.Load() if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1) if err != nil { return errors.New("database unavailable") } defer db.Close() inventory, err := mysql.ReadRetentionInventory(ctx, db, cutoff) encoded, encodeErr := json.Marshal(inventory) if encodeErr != nil { return encodeErr } fmt.Println(string(encoded)) return err } func runBootstrap(args []string) error { flags := flag.NewFlagSet("bootstrap", flag.ContinueOnError) var in mysql.BootstrapInput var output string flags.StringVar(&in.Application, "application", "", "new application name") flags.Func("application-id", "existing application id; mutually exclusive with --application", func(v string) error { in.ExistingApplicationID = core.ID(v) return in.ExistingApplicationID.Validate() }) flags.BoolVar(&in.ResourceAccess, "resource-access", false, "grant application-wide resource read and manage actions") flags.StringVar(&in.Namespace, "namespace", "", "new namespace name") flags.StringVar(&in.Environment, "environment", "", "environment name") flags.StringVar(&in.Queue, "queue", "", "initial queue name") flags.StringVar(&output, "output", "", "new private credentials JSON file") flags.BoolVar(&in.WorkerAccess, "worker-access", false, "also grant Worker connect and consume actions for development") flags.BoolVar(&in.ScheduleAccess, "schedule-access", false, "also grant schedule read and manage actions for development") flags.BoolVar(&in.EventAccess, "event-access", false, "also grant result subscription read, manage and consume actions") flags.BoolVar(&in.RecoveryAccess, "recovery-access", false, "explicitly grant task review, manual retry and operation audit read actions") flags.BoolVar(&in.WorkflowAccess, "workflow-access", false, "explicitly grant workflow definition read and publish actions") flags.BoolVar(&in.WorkflowRunAccess, "workflow-run-access", false, "explicitly grant workflow start and read actions") flags.BoolVar(&in.WorkflowControlAccess, "workflow-control-access", false, "explicitly grant workflow control and read actions") flags.BoolVar(&in.WorkflowSignalAccess, "workflow-signal-access", false, "explicitly grant workflow signal and read actions") flags.BoolVar(&in.WorkflowRecoveryAccess, "workflow-recovery-access", false, "explicitly grant workflow recovery and read actions") flags.BoolVar(&in.WorkflowAuditAccess, "workflow-audit-access", false, "explicitly grant workflow operation audit read action") if err := flags.Parse(args); err != nil { return err } if flags.NArg() != 0 || output == "" { return errors.New("--output and named resource arguments are required") } cfg, err := config.Load() if err != nil { return err } file, err := os.OpenFile(output, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) if err != nil { return errors.New("cannot create new private output file (existing files are never overwritten)") } defer file.Close() delivered := false defer func() { if !delivered { _ = os.Remove(output) } }() ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1) if err != nil { return err } defer db.Close() err = mysql.Bootstrap(ctx, db, in, func(result mysql.BootstrapResult) error { if err := json.NewEncoder(file).Encode(result); err != nil { return err } if err := file.Sync(); err != nil { return err } directory, err := os.Open(filepath.Dir(output)) if err != nil { return err } err = directory.Sync() closeErr := directory.Close() if err != nil { return err } if closeErr != nil { return closeErr } delivered = true return nil }) if err != nil { if delivered { return errors.New("bootstrap commit could not be confirmed; private output was retained; verify its credential before retrying") } return errors.New("bootstrap failed before credential delivery; check schema, names and database access") } fmt.Println("Bootstrap committed. Credentials saved in the requested private file; valid for 30 days.") return nil }