M1.5: NodeConfig dynamic informer + RBAC
Build flock Image / build (push) Has been cancelled

Agent now watches nodeconfigs.flock.fritzlab.net via a client-go dynamic
informer, filters events to its own node name, and caches the typed
NodeConfig in memory (NodeConfigCache, atomic pointer). M2's IPAM will
read from that cache.

- pkg/agent/nodeconfig.go: informer + JSON-round-trip decode (avoids
  hand-written DeepCopy + scheme registration for this small a use).
- pkg/agent/server.go: starts the informer goroutine; Run terminates if
  the informer returns.
- pkg/api/v1alpha1: switch placeholder TypeMeta/ObjectMeta to metav1.
- deploy/rbac: get/list/watch on nodeconfigs.
- cmd/flock-agent: --kubeconfig flag for out-of-cluster runs (tests).

Satisfies M1 verified-by: "kubectl apply NodeConfig; agent logs read it".

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-24 22:00:48 -05:00
parent e0ae98ad6c
commit 759ed21b37
8 changed files with 383 additions and 72 deletions
+53 -24
View File
@@ -7,6 +7,9 @@ import (
"net"
"os"
"path/filepath"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// SocketPath is the unix socket on which flock-agent serves RPCs from the
@@ -15,22 +18,24 @@ import (
const SocketPath = "/run/flock/flock.sock"
// Server is the agent's runtime container: state store, kubernetes informers,
// netlink, BIRD, nftables. M1 wires only the state store and a placeholder
// listener so the binary boots and exits cleanly under a context.
// netlink, BIRD, nftables. M1.5 wires the state store, a placeholder unix
// listener, and a NodeConfig informer.
type Server struct {
Node string
Store *Store
Logger *slog.Logger
socket string
closeCh chan struct{}
Node string
Store *Store
NodeConfig *NodeConfigCache
Logger *slog.Logger
socket string
restCfg *rest.Config
}
// Config configures NewServer.
type Config struct {
Node string
StatePath string // typically /var/lib/flock/allocations.json
Socket string // typically /run/flock/flock.sock
Logger *slog.Logger
Node string
StatePath string // typically /var/lib/flock/allocations.json
Socket string // typically /run/flock/flock.sock
Logger *slog.Logger
Kubeconfig string // empty => in-cluster config
}
// NewServer constructs a Server. It does NOT start any goroutines; call Run.
@@ -54,18 +59,32 @@ func NewServer(cfg Config) (*Server, error) {
if err != nil {
return nil, fmt.Errorf("open store: %w", err)
}
restCfg, err := loadRestConfig(cfg.Kubeconfig)
if err != nil {
return nil, fmt.Errorf("load kube config: %w", err)
}
return &Server{
Node: cfg.Node,
Store: store,
Logger: cfg.Logger,
socket: cfg.Socket,
closeCh: make(chan struct{}),
Node: cfg.Node,
Store: store,
NodeConfig: &NodeConfigCache{},
Logger: cfg.Logger,
socket: cfg.Socket,
restCfg: restCfg,
}, nil
}
// Run starts the agent and blocks until ctx is cancelled. M1 only opens the
// unix listener (proving permissions/path); the RPC handler is a no-op
// returning ENOSYS until M2.
func loadRestConfig(kubeconfig string) (*rest.Config, error) {
if kubeconfig != "" {
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
return rest.InClusterConfig()
}
// Run starts the agent and blocks until ctx is cancelled. M1.5 opens the
// unix listener, starts the NodeConfig informer, and waits. The RPC handler
// is still a no-op until M2.
func (s *Server) Run(ctx context.Context) error {
if err := os.MkdirAll(filepath.Dir(s.socket), 0o750); err != nil {
return fmt.Errorf("mkdir socket dir: %w", err)
@@ -83,18 +102,28 @@ func (s *Server) Run(ctx context.Context) error {
"allocations", len(s.Store.Snapshot()),
)
// Accept loop: M1 closes every accepted conn immediately. M2 will dispatch.
// Accept loop: closes every connection immediately (M2 will dispatch).
go func() {
for {
conn, err := l.Accept()
if err != nil {
return // listener closed
return
}
_ = conn.Close()
}
}()
<-ctx.Done()
s.Logger.Info("flock-agent stopping")
return nil
// NodeConfig informer. Any error from the informer terminates Run.
errCh := make(chan error, 1)
go func() {
errCh <- StartNodeConfigInformer(ctx, s.restCfg, s.Node, s.NodeConfig, s.Logger)
}()
select {
case <-ctx.Done():
s.Logger.Info("flock-agent stopping")
return nil
case err := <-errCh:
return fmt.Errorf("informer: %w", err)
}
}