CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 36s
CI / go (push) Successful in 54s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m46s
- Enhanced AGENTS.md to include post-editing commands for Go code, specifying the use of `gofmt -w`, `go vet ./...`, and `scripts/lint-go.ps1`. - Updated engineering rules in .cursor/rules/engineering.mdc to clarify the linting process and ensure consistency in Go code formatting. - Improved documentation for CI checks related to Go code style and linting.
119 lines
3.4 KiB
Go
119 lines
3.4 KiB
Go
package nodecli
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/birdfmt"
|
|
"evobgp/internal/bundle"
|
|
"evobgp/internal/signing"
|
|
)
|
|
|
|
// SyncConfig drives pull → verify → apply on a replica node.
|
|
type SyncConfig struct {
|
|
BaseURL string
|
|
Token string
|
|
SpeakerID string
|
|
RevisionID string // empty = latest published on CP
|
|
PubKeyB64 string
|
|
PubKeyHex string
|
|
ExtractDir string
|
|
BundlePath string // temp file; default os.TempDir()/evobgp-bundle.tar.gz
|
|
BirdBin string
|
|
BirdcBin string
|
|
Socket string
|
|
HTTPClient interface {
|
|
Do(req interface{}) (interface{}, error)
|
|
}
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// SyncResult summarizes a successful sync.
|
|
type SyncResult struct {
|
|
RevisionID string `json:"revision_id"`
|
|
MainConfig string `json:"main_config,omitempty"`
|
|
}
|
|
|
|
// SyncBundle pulls (if needed), verifies Ed25519 signature, extracts, parse-checks, and birdc configure.
|
|
func SyncBundle(ctx context.Context, cfg SyncConfig) (SyncResult, error) {
|
|
if strings.TrimSpace(cfg.BaseURL) == "" || strings.TrimSpace(cfg.Token) == "" || strings.TrimSpace(cfg.SpeakerID) == "" {
|
|
return SyncResult{}, fmt.Errorf("nodecli: sync: base-url, token, speaker-id required")
|
|
}
|
|
if strings.TrimSpace(cfg.ExtractDir) == "" {
|
|
return SyncResult{}, fmt.Errorf("nodecli: sync: extract-dir required")
|
|
}
|
|
pub, err := loadPubKey(cfg.PubKeyB64, cfg.PubKeyHex)
|
|
if err != nil {
|
|
return SyncResult{}, fmt.Errorf("nodecli: sync: %w", err)
|
|
}
|
|
timeout := cfg.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
rev := strings.TrimSpace(cfg.RevisionID)
|
|
if rev == "" {
|
|
var err error
|
|
rev, err = fetchLatestRevision(cfg.BaseURL, cfg.Token, cfg.SpeakerID)
|
|
if err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
}
|
|
raw, err := fetchBundle(cfg.BaseURL, cfg.Token, cfg.SpeakerID, rev)
|
|
if err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
bundlePath := strings.TrimSpace(cfg.BundlePath)
|
|
if bundlePath == "" {
|
|
bundlePath = filepath.Join(os.TempDir(), "evobgp-bundle.tar.gz")
|
|
}
|
|
if err := os.WriteFile(bundlePath, raw, 0o644); err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
v, err := signing.VerifyGzippedTar(raw, pub)
|
|
if err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
root := filepath.Clean(cfg.ExtractDir)
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
if err := bundle.WriteExtractedFiles(root, v); err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
mainRel := v.FindMainBirdConf()
|
|
if mainRel == "" {
|
|
return SyncResult{}, fmt.Errorf("nodecli: sync: bundle has no bird.conf in manifest")
|
|
}
|
|
mainPath := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(mainRel, "/")))
|
|
opCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
ctl := &birdfmt.BirdCtl{Bird: cfg.BirdBin, Birdc: cfg.BirdcBin, Socket: cfg.Socket}
|
|
if err := ctl.ParseCheck(opCtx, mainPath); err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
if err := ctl.Configure(opCtx); err != nil {
|
|
return SyncResult{}, err
|
|
}
|
|
return SyncResult{RevisionID: rev, MainConfig: mainPath}, nil
|
|
}
|
|
|
|
// SyncResultJSON encodes SyncResult for HTTP responses.
|
|
func SyncResultJSON(r SyncResult) ([]byte, error) {
|
|
return json.Marshal(map[string]any{
|
|
"ok": true,
|
|
"applied_revision_id": r.RevisionID,
|
|
"main_config": r.MainConfig,
|
|
})
|
|
}
|
|
|
|
// LoadPublicKey exports loadPubKey for other packages.
|
|
func LoadPublicKey(pubB64, pubHex string) (ed25519.PublicKey, error) {
|
|
return loadPubKey(pubB64, pubHex)
|
|
}
|