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
153 lines
4.4 KiB
Go
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
|
|
}
|