feat: bind schema verification to release manifest
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user