Files
EvoBGP/internal/bundle/pack.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

162 lines
4.2 KiB
Go

package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
// Manifest describes bundle contents for evobgp-node verification.
type Manifest struct {
RevisionID string `json:"revision_id"`
SpeakerID string `json:"speaker_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
Files []FileEntry `json:"files"`
Algorithm string `json:"signature_algorithm"`
PublicKeyB64 string `json:"public_key_base64"`
}
// FileEntry is one file inside the bundle archive.
type FileEntry struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
}
const sigFileName = "bundle.sig"
const manifestName = "manifest.json"
// BuildGzippedTar builds a .tar.gz with manifest.json, bundle.sig (Ed25519 over manifest JSON), and payload files.
func BuildGzippedTar(revisionID, speakerID string, fragments map[string]string, priv ed25519.PrivateKey) ([]byte, error) {
return BuildGzippedTarWithLimits(revisionID, speakerID, fragments, priv, Limits{})
}
// BuildGzippedTarWithLimits preserves the manifest/signature wire format while
// refusing payloads that exceed the receiving node's configured budgets.
func BuildGzippedTarWithLimits(revisionID, speakerID string, fragments map[string]string, priv ed25519.PrivateKey, limits Limits) ([]byte, error) {
l, err := limits.Normalized()
if err != nil {
return nil, err
}
if len(priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("bundle: invalid ed25519 private key size")
}
pub := priv.Public().(ed25519.PublicKey)
norm := make(map[string]string)
seen := map[string]bool{}
var total int64
for p, content := range fragments {
if strings.HasPrefix(p, "_") {
continue // UI-only preview aggregate, not shipped to nodes
}
if len(norm) >= l.MaxEntries-2 {
return nil, ErrLimit
}
if err := validatePath(p, l.MaxPathBytes); err != nil {
return nil, err
}
if p == manifestName || p == sigFileName || seen[strings.ToLower(p)] {
return nil, fmt.Errorf("bundle: duplicate or reserved path %q", p)
}
seen[strings.ToLower(p)] = true
if int64(len(content)) > l.MaxFileBytes || int64(len(content)) > l.MaxExpandedBytes-total {
return nil, ErrLimit
}
total += int64(len(content))
norm[p] = content
}
if len(norm) > l.MaxEntries-2 {
return nil, ErrLimit
}
var files []FileEntry
var payload [][]byte
keys := sortedStringKeys(norm)
for _, p := range keys {
data := []byte(norm[p])
h := sha256.Sum256(data)
files = append(files, FileEntry{Path: p, SHA256: fmt.Sprintf("%x", h[:])})
payload = append(payload, data)
}
m := Manifest{
RevisionID: revisionID,
SpeakerID: speakerID,
CreatedAt: time.Now().UTC(),
Files: files,
Algorithm: "ed25519",
PublicKeyB64: base64.StdEncoding.EncodeToString(pub),
}
manifestJSON, err := json.MarshalIndent(m, "", " ")
if err != nil {
return nil, err
}
if int64(len(manifestJSON)) > l.MaxManifestBytes {
return nil, ErrLimit
}
sig := ed25519.Sign(priv, manifestJSON)
var buf bytes.Buffer
gw := gzip.NewWriter(&limitedWriter{w: &buf, remaining: l.MaxCompressedBytes})
tw := tar.NewWriter(&limitedWriter{w: gw, remaining: l.MaxExpandedBytes})
add := func(name string, body []byte) error {
hdr := &tar.Header{
Name: name,
Mode: 0o644,
Size: int64(len(body)),
ModTime: time.Now().UTC(),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
_, err := tw.Write(body)
return err
}
if err := add(manifestName, manifestJSON); err != nil {
_ = tw.Close()
_ = gw.Close()
return nil, err
}
if err := add(sigFileName, sig); err != nil {
_ = tw.Close()
_ = gw.Close()
return nil, err
}
i := 0
for _, p := range keys {
if err := add(p, payload[i]); err != nil {
_ = tw.Close()
_ = gw.Close()
return nil, err
}
i++
}
if err := tw.Close(); err != nil {
_ = gw.Close()
return nil, err
}
if err := gw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func sortedStringKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}