Files
Denozordec 693227163a
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m0s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m31s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m35s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m17s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
refactor: improve handling of deployable fragments in ApplyRevision and buildPreviewFragments functions. Introduce isDeployableBirdFragment to filter out UI-only preview keys, and update related logic in the API to ensure proper job queuing and response handling for module refresh operations.
2026-04-05 23:33:56 +07:00

153 lines
4.4 KiB
Go

// Package birddeploy applies revision preview fragments to a shared BIRD volume with parse-check and LKG (plan §13).
package birddeploy
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"evobgp/internal/birdfmt"
"evobgp/internal/store"
)
// Config drives two-phase deploy from a revision in the store.
type Config struct {
ActiveDir string // e.g. /etc/bird (live config tree)
StagingDir string // e.g. /tmp/evobgp-staging (same host/volume as ActiveDir)
BirdBin string
BirdcBin string
Socket string // birdc -s
}
// ApplyRevision writes preview fragments to staging, runs bird -p, swaps into active, runs birdc configure; restores LKG on failure.
func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revision, cfg Config) error {
if rev == nil || len(rev.PreviewFragments) == 0 {
return fmt.Errorf("birddeploy: no preview fragments")
}
active := strings.TrimSpace(cfg.ActiveDir)
staging := strings.TrimSpace(cfg.StagingDir)
if active == "" || staging == "" {
return fmt.Errorf("birddeploy: ActiveDir and StagingDir required")
}
if err := os.MkdirAll(staging, 0o755); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(active, "bird.d"), 0o755); err != nil {
return err
}
lkg := filepath.Join(active, ".evobgp_lkg")
// Phase 1: write staging tree
for rel, content := range rev.PreviewFragments {
rel = strings.TrimPrefix(rel, "/")
if !isDeployableBirdFragment(rel) {
continue
}
dst := filepath.Join(staging, rel)
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
if err := os.WriteFile(dst, []byte(content), 0o644); err != nil {
return err
}
}
mainConf := filepath.Join(staging, "bird.conf")
if _, err := os.Stat(mainConf); err != nil {
return fmt.Errorf("birddeploy: staging missing bird.conf: %w", err)
}
bc := ctl
if bc == nil {
bc = &birdfmt.BirdCtl{Bird: cfg.BirdBin, Birdc: cfg.BirdcBin, Socket: cfg.Socket}
}
if err := bc.ParseCheck(ctx, mainConf); err != nil {
return fmt.Errorf("birddeploy: bird -p failed: %w", err)
}
// Snapshot LKG (best-effort copy of current active bird.conf + bird.d)
_ = os.RemoveAll(lkg)
_ = snapshotBirdTree(active, lkg)
// Phase 2: atomic swap into active
for rel, content := range rev.PreviewFragments {
rel = strings.TrimPrefix(rel, "/")
if !isDeployableBirdFragment(rel) {
continue
}
dst := filepath.Join(active, rel)
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
_ = restoreLKG(lkg, active)
return err
}
tmp := dst + ".tmp"
if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
_ = restoreLKG(lkg, active)
return err
}
if err := os.Rename(tmp, dst); err != nil {
_ = restoreLKG(lkg, active)
return err
}
}
if err := bc.Configure(ctx); err != nil {
_ = restoreLKG(lkg, active)
_ = bc.Configure(ctx)
return fmt.Errorf("birddeploy: birdc configure failed, restored LKG: %w", err)
}
return nil
}
// isDeployableBirdFragment skips UI-only preview keys (e.g. flattened full config) not referenced from bird.conf.
func isDeployableBirdFragment(rel string) bool {
return rel != "" && !strings.HasPrefix(rel, "_")
}
func snapshotBirdTree(activeRoot, dstRoot string) error {
if err := os.MkdirAll(dstRoot, 0o755); err != nil {
return err
}
mc := filepath.Join(activeRoot, "bird.conf")
if b, err := os.ReadFile(mc); err == nil {
_ = os.WriteFile(filepath.Join(dstRoot, "bird.conf"), b, 0o644)
}
bd := filepath.Join(activeRoot, "bird.d")
if st, err := os.Stat(bd); err == nil && st.IsDir() {
_ = filepath.WalkDir(bd, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
suffix, _ := filepath.Rel(activeRoot, path)
out := filepath.Join(dstRoot, suffix)
_ = os.MkdirAll(filepath.Dir(out), 0o755)
b, err := os.ReadFile(path)
if err != nil {
return nil
}
return os.WriteFile(out, b, 0o644)
})
}
return nil
}
func restoreLKG(lkg, active string) error {
if _, err := os.Stat(filepath.Join(lkg, "bird.conf")); err != nil {
return err
}
_ = filepath.WalkDir(lkg, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
suffix, _ := filepath.Rel(lkg, path)
out := filepath.Join(active, suffix)
_ = os.MkdirAll(filepath.Dir(out), 0o755)
b, err := os.ReadFile(path)
if err != nil {
return nil
}
return os.WriteFile(out, b, 0o644)
})
return nil
}