feat: bind schema verification to release manifest

This commit is contained in:
2026-09-20 21:23:30 +08:00
parent cebb32138c
commit 30a8081bd9
7 changed files with 95 additions and 6 deletions

View File

@@ -6,8 +6,10 @@ import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
@@ -69,9 +71,17 @@ 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 errors.New("release migration manifest unavailable")
return releaseManifest{}, errors.New("release migration manifest unavailable")
}
items := make([]releaseMigrationManifest, 0, len(migrations))
for _, migration := range migrations {
@@ -81,7 +91,7 @@ func runRelease(args []string) error {
Checksum: migration.Checksum,
})
}
manifest := releaseManifest{
return releaseManifest{
ManifestVersion: 1,
Build: buildinfo.Current(),
Schema: releaseSchemaManifest{
@@ -89,18 +99,52 @@ func runRelease(args []string) error {
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")
}
return json.NewEncoder(os.Stdout).Encode(manifest)
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

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"testing"
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
@@ -51,6 +52,35 @@ func TestReleaseManifestRejectsUnknownCommand(t *testing.T) {
}
}
func TestReleaseManifestValidationBindsBinaryIdentity(t *testing.T) {
manifest, err := currentReleaseManifest()
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "release-manifest.json")
encoded, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, encoded, 0600); err != nil {
t.Fatal(err)
}
if err := validateReleaseManifest(path); err != nil {
t.Fatalf("matching manifest rejected: %v", err)
}
manifest.Build.Revision = "different-release"
encoded, err = json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, encoded, 0600); err != nil {
t.Fatal(err)
}
if err := validateReleaseManifest(path); err == nil {
t.Fatal("manifest with different revision accepted")
}
}
func captureStdout(t *testing.T) (*bytes.Buffer, func()) {
t.Helper()
old := os.Stdout