feat: add release identity manifest
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"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"
|
||||
@@ -23,9 +24,11 @@ func main() {
|
||||
}
|
||||
func run() error {
|
||||
if len(os.Args) < 2 {
|
||||
return errors.New("usage: orbit-admin bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
||||
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":
|
||||
@@ -37,7 +40,57 @@ func run() error {
|
||||
case "credential":
|
||||
return runCredential(os.Args[2:])
|
||||
}
|
||||
return errors.New("usage: orbit-admin bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
||||
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")
|
||||
}
|
||||
migrations, err := mysql.Migrations()
|
||||
if err != nil || len(migrations) == 0 {
|
||||
return 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,
|
||||
})
|
||||
}
|
||||
manifest := releaseManifest{
|
||||
ManifestVersion: 1,
|
||||
Build: buildinfo.Current(),
|
||||
Schema: releaseSchemaManifest{
|
||||
CurrentVersion: migrations[len(migrations)-1].Version,
|
||||
MigrationCount: len(items),
|
||||
Migrations: items,
|
||||
},
|
||||
}
|
||||
return json.NewEncoder(os.Stdout).Encode(manifest)
|
||||
}
|
||||
|
||||
func runVerify(args []string) error {
|
||||
|
||||
74
cmd/orbit-admin/release_test.go
Normal file
74
cmd/orbit-admin/release_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
||||
)
|
||||
|
||||
func TestReleaseManifestIsDatabaseIndependent(t *testing.T) {
|
||||
output, restore := captureStdout(t)
|
||||
err := runRelease([]string{"manifest"})
|
||||
restore()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got releaseManifest
|
||||
if err := json.Unmarshal(output.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
migrations, err := mysql.Migrations()
|
||||
if err != nil || len(migrations) == 0 {
|
||||
t.Fatalf("migrations: %v", err)
|
||||
}
|
||||
if got.ManifestVersion != 1 {
|
||||
t.Fatalf("manifest version=%d", got.ManifestVersion)
|
||||
}
|
||||
if got.Build != buildinfo.Current() {
|
||||
t.Fatalf("build identity=%+v want=%+v", got.Build, buildinfo.Current())
|
||||
}
|
||||
if got.Schema.CurrentVersion != migrations[len(migrations)-1].Version || got.Schema.MigrationCount != len(migrations) {
|
||||
t.Fatalf("schema summary=%+v migrations=%d", got.Schema, len(migrations))
|
||||
}
|
||||
if len(got.Schema.Migrations) != len(migrations) {
|
||||
t.Fatalf("manifest migrations=%d want=%d", len(got.Schema.Migrations), len(migrations))
|
||||
}
|
||||
for i, migration := range migrations {
|
||||
item := got.Schema.Migrations[i]
|
||||
if item.Version != migration.Version || item.Name != migration.Name || item.Checksum != migration.Checksum {
|
||||
t.Fatalf("migration[%d]=%+v want version=%d name=%s checksum=%s", i, item, migration.Version, migration.Name, migration.Checksum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseManifestRejectsUnknownCommand(t *testing.T) {
|
||||
if err := runRelease([]string{"status"}); err == nil {
|
||||
t.Fatal("unknown release command accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T) (*bytes.Buffer, func()) {
|
||||
t.Helper()
|
||||
old := os.Stdout
|
||||
read, write, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = write
|
||||
var output bytes.Buffer
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, _ = output.ReadFrom(read)
|
||||
close(done)
|
||||
}()
|
||||
return &output, func() {
|
||||
_ = write.Close()
|
||||
os.Stdout = old
|
||||
<-done
|
||||
_ = read.Close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user