Add tomswall agent (control-plane pull mode)
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.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Client talks to the tomswallapi control plane for one device.
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
Device string
|
||||
Token string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
// NewClient builds a Client with a sane default timeout.
|
||||
func NewClient(baseURL, device, token string) *Client {
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
Device: device,
|
||||
Token: token,
|
||||
HTTP: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// FetchConfig retrieves the device's rendered config. It returns both the parsed
|
||||
// document and the raw bytes (so callers can cache exactly what was served).
|
||||
func (c *Client) FetchConfig(ctx context.Context) (*RenderedConfig, []byte, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/devices/%s/config", c.BaseURL, c.Device)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
req.Header.Set("Accept", "application/yaml")
|
||||
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("fetch config: status %d: %s", resp.StatusCode, bytes.TrimSpace(body))
|
||||
}
|
||||
|
||||
cfg, err := ParseRendered(body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return cfg, body, nil
|
||||
}
|
||||
|
||||
// ParseRendered decodes a rendered config document (YAML, JSON is a subset).
|
||||
func ParseRendered(body []byte) (*RenderedConfig, error) {
|
||||
var cfg RenderedConfig
|
||||
if err := yaml.Unmarshal(body, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing rendered config: %w", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ReportStatus tells the control plane which generation this device has applied.
|
||||
func (c *Client) ReportStatus(ctx context.Context, generation int64) error {
|
||||
url := fmt.Sprintf("%s/api/v1/devices/%s/status", c.BaseURL, c.Device)
|
||||
payload, _ := json.Marshal(map[string]int64{"generation": generation})
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<16))
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("report status: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user