Files
Denozordec 6fa265a246
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
docs: update Go code style guidelines and linting requirements
- 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.
2026-05-21 12:50:14 +07:00

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)
}