e0f54ef320
Add `tomswall agent`: it pulls this device's compiled config from tomswallapi, differentially applies it, and reports the applied generation. It caches the last known-good config and, when the control plane is unreachable, keeps applying that cache — it never fails closed. - internal/agent: rendered-config types, HTTP client (fetch + status report), on-disk cache, on-device DNS resolver for dns sets (honors the device's configured resolver, fail-safe on lookup failure), and the pull-apply-report loop behind a mockable Applier. - Translate the interface-agnostic, address-matched rendered model into native tomswall config using the "all:<cidr>" any-interface source/dest form, reusing the existing differential engine. Named-set members are inlined as concrete addresses (native nft set references are a tracked follow-up). - cmd/tomswall: wire the `agent` subcommand (flags + TOMSWALL_* env, --once). - Unit tests: translation, cache, and the don't-fail-closed fallback loop. - Add DESIGN.md documenting the control-plane architecture.
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.unkin.net/unkin/tomswall/internal/agent"
|
|
)
|
|
|
|
func agentCmd() *cobra.Command {
|
|
var (
|
|
apiURL string
|
|
device string
|
|
token string
|
|
cachePath string
|
|
interval time.Duration
|
|
once bool
|
|
)
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "agent",
|
|
Short: "Pull compiled config from tomswallapi and apply it continuously",
|
|
Long: `Agent runs the control-plane pull loop: it fetches this device's compiled
|
|
config from tomswallapi, differentially applies it, and reports the applied
|
|
generation back. It caches the last known-good config and, if the control plane
|
|
is unreachable, keeps applying that cache — it never fails closed.
|
|
|
|
The agent token defaults to the TOMSWALL_AGENT_TOKEN environment variable, and
|
|
the device name defaults to the system hostname.`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if token == "" {
|
|
token = os.Getenv("TOMSWALL_AGENT_TOKEN")
|
|
}
|
|
if device == "" {
|
|
device, _ = os.Hostname()
|
|
}
|
|
if apiURL == "" {
|
|
return fmt.Errorf("--api-url is required (or set it in the environment)")
|
|
}
|
|
if device == "" {
|
|
return fmt.Errorf("--device is required (could not determine hostname)")
|
|
}
|
|
if token == "" {
|
|
return fmt.Errorf("agent token required: set --token or TOMSWALL_AGENT_TOKEN")
|
|
}
|
|
|
|
a := &agent.Agent{
|
|
Client: agent.NewClient(apiURL, device, token),
|
|
Cache: agent.Cache{Path: cachePath},
|
|
Interval: interval,
|
|
Applier: agent.EngineApplier{},
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
if once {
|
|
return a.RunOnce(ctx)
|
|
}
|
|
return a.Run(ctx)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&apiURL, "api-url", os.Getenv("TOMSWALL_API_URL"), "tomswallapi base URL (or TOMSWALL_API_URL)")
|
|
cmd.Flags().StringVar(&device, "device", "", "device name (defaults to hostname)")
|
|
cmd.Flags().StringVar(&token, "token", "", "agent bearer token (or TOMSWALL_AGENT_TOKEN)")
|
|
cmd.Flags().StringVar(&cachePath, "cache", "/var/lib/tomswall/rendered.yaml", "path to the last-known-good config cache")
|
|
cmd.Flags().DurationVar(&interval, "interval", time.Minute, "poll interval")
|
|
cmd.Flags().BoolVar(&once, "once", false, "run a single apply cycle and exit")
|
|
return cmd
|
|
}
|