feat(remote-speakers): enhance remote speaker management and API integration
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Failing after 34s
CI / go (push) Failing after 19s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Failing after 34s
CI / go (push) Failing after 19s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
- Added support for remote speaker configuration in the README and documentation. - Implemented a new endpoint for retrieving the bundle signing public key. - Updated the `evobgp-agent` to include a `serve` command for Panel→Node sync API. - Enhanced CI workflow to validate remote speaker compose files. - Introduced new fields in the API and UI for managing speaker metadata, including dispatch status and sync status. - Improved error handling and response formatting in speaker-related API endpoints. - Updated documentation to reflect changes in remote speaker functionality and usage guidelines.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user