diff --git a/CHANGELOG.md b/CHANGELOG.md index 130a2dc59..698c623b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -236,6 +236,8 @@ All notable changes to this project will be documented in this file. - feat(smartcontract): add use_onchain_deallocation flag to MulticastGroup ([#2748](https://github.com/malbeclabs/doublezero/pull/2748)) - CLI - Remove restriction for a single tunnel per user; now a user can have a unicast and multicast tunnel concurrently (but can only be a publisher _or_ a subscriber) ([2728](https://github.com/malbeclabs/doublezero/pull/2728)) +- Device agents + - Reduce config agent network and CPU usage by checking config checksums every 5 seconds, and reducing full config check frquency to 1m ## [v0.8.3](https://github.com/malbeclabs/doublezero/compare/client/v0.8.2...client/v0.8.3) – 2026-01-22 diff --git a/controlplane/agent/cmd/agent/main.go b/controlplane/agent/cmd/agent/main.go index 7114b17ff..cb9ee6a7b 100644 --- a/controlplane/agent/cmd/agent/main.go +++ b/controlplane/agent/cmd/agent/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/sha256" + "encoding/hex" "flag" "fmt" "log" @@ -20,16 +22,17 @@ import ( ) var ( - localDevicePubkey = flag.String("pubkey", "frtyt4WKYudUpqTsvJzwN6Bd4btYxrkaYNhBNAaUVGWn", "This device's public key on the doublezero network") - controllerAddress = flag.String("controller", "18.116.166.35:7000", "The DoubleZero controller IP address and port to connect to") - device = flag.String("device", "127.0.0.1:9543", "IP Address and port of the Arist EOS API. Should always be the local switch at 127.0.0.1:9543.") - sleepIntervalInSeconds = flag.Float64("sleep-interval-in-seconds", 5, "How long to sleep in between polls") - controllerTimeoutInSeconds = flag.Float64("controller-timeout-in-seconds", 30, "How long to wait for a response from the controller before giving up") - maxLockAge = flag.Int("max-lock-age-in-seconds", 3600, "If agent detects a config lock that older than the specified age, it will force unlock.") - verbose = flag.Bool("verbose", false, "Enable verbose logging") - showVersion = flag.Bool("version", false, "Print the version of the doublezero-agent and exit") - metricsEnable = flag.Bool("metrics-enable", false, "Enable prometheus metrics") - metricsAddr = flag.String("metrics-addr", ":8080", "Address to listen on for prometheus metrics") + localDevicePubkey = flag.String("pubkey", "frtyt4WKYudUpqTsvJzwN6Bd4btYxrkaYNhBNAaUVGWn", "This device's public key on the doublezero network") + controllerAddress = flag.String("controller", "18.116.166.35:7000", "The DoubleZero controller IP address and port to connect to") + device = flag.String("device", "127.0.0.1:9543", "IP Address and port of the Arist EOS API. Should always be the local switch at 127.0.0.1:9543.") + sleepIntervalInSeconds = flag.Float64("sleep-interval-in-seconds", 5, "How long to sleep in between polls") + controllerTimeoutInSeconds = flag.Float64("controller-timeout-in-seconds", 30, "How long to wait for a response from the controller before giving up") + configCacheTimeoutInSeconds = flag.Int("config-cache-timeout-in-seconds", 60, "Force full config fetch after this many seconds, even if hash unchanged") + maxLockAge = flag.Int("max-lock-age-in-seconds", 3600, "If agent detects a config lock that older than the specified age, it will force unlock.") + verbose = flag.Bool("verbose", false, "Enable verbose logging") + showVersion = flag.Bool("version", false, "Print the version of the doublezero-agent and exit") + metricsEnable = flag.Bool("metrics-enable", false, "Enable prometheus metrics") + metricsAddr = flag.String("metrics-addr", ":8080", "Address to listen on for prometheus metrics") // set by LDFLAGS version = "dev" @@ -37,35 +40,33 @@ var ( date = "unknown" ) -func pollControllerAndConfigureDevice(ctx context.Context, dzclient pb.ControllerClient, eapiClient *arista.EAPIClient, pubkey string, verbose *bool, maxLockAge int, agentVersion string, agentCommit string, agentDate string) error { - var err error - - // The dz controller needs to know what BGP sessions we have configured locally - var neighborIpMap map[string][]string - neighborIpMap, err = eapiClient.GetBgpNeighbors(ctx) - if err != nil { - log.Println("pollControllerAndConfigureDevice: eapiClient.GetBgpNeighbors returned error:", err) - agent.ErrorsBgpNeighbors.Inc() - } +func computeChecksum(data string) string { + hash := sha256.Sum256([]byte(data)) + return hex.EncodeToString(hash[:]) +} - var configText string +func fetchConfigFromController(ctx context.Context, dzclient pb.ControllerClient, pubkey string, neighborIpMap map[string][]string, verbose *bool, agentVersion string, agentCommit string, agentDate string) (configText string, configHash string, err error) { configText, err = agent.GetConfigFromServer(ctx, dzclient, pubkey, neighborIpMap, controllerTimeoutInSeconds, agentVersion, agentCommit, agentDate) if err != nil { - log.Printf("pollControllerAndConfigureDevice failed to call agent.GetConfigFromServer: %q", err) + log.Printf("fetchConfigFromController failed to call agent.GetConfigFromServer: %q", err) agent.ErrorsGetConfig.Inc() - return err + return "", "", err } if *verbose { log.Printf("controller returned the following config: '%s'", configText) } + configHash = computeChecksum(configText) + return configText, configHash, nil +} + +func applyConfig(ctx context.Context, eapiClient *arista.EAPIClient, configText string, maxLockAge int) error { if configText == "" { - // Controller returned empty config return nil } - _, err = eapiClient.AddConfigToDevice(ctx, configText, nil, maxLockAge) // 3rd arg (diffCmd) is only used for testing + _, err := eapiClient.AddConfigToDevice(ctx, configText, nil, maxLockAge) if err != nil { agent.ErrorsApplyConfig.Inc() return err @@ -121,15 +122,55 @@ func main() { client := aristapb.NewEapiMgrServiceClient(clientConn) eapiClient = arista.NewEAPIClient(slog.Default(), client) + var cachedConfigHash string + var configCacheTime time.Time + configCacheTimeout := time.Duration(*configCacheTimeoutInSeconds) * time.Second + for { select { case <-ctx.Done(): return case <-ticker.C: - err := pollControllerAndConfigureDevice(ctx, dzclient, eapiClient, *localDevicePubkey, verbose, *maxLockAge, version, commit, date) + neighborIpMap, err := eapiClient.GetBgpNeighbors(ctx) + if err != nil { + log.Println("ERROR: eapiClient.GetBgpNeighbors returned", err) + agent.ErrorsBgpNeighbors.Inc() + } + + shouldFetchAndApply := false + + if cachedConfigHash == "" { + shouldFetchAndApply = true + } else if time.Since(configCacheTime) >= configCacheTimeout { + shouldFetchAndApply = true + } else { + hash, err := agent.GetConfigHashFromServer(ctx, dzclient, *localDevicePubkey, neighborIpMap, controllerTimeoutInSeconds, version, commit, date) + if err != nil { + log.Println("ERROR: GetConfigHashFromServer returned", err) + continue + } + if hash != cachedConfigHash { + shouldFetchAndApply = true + } + } + + if !shouldFetchAndApply { + continue + } + + configText, configHash, err := fetchConfigFromController(ctx, dzclient, *localDevicePubkey, neighborIpMap, verbose, version, commit, date) + if err != nil { + log.Println("ERROR: fetchConfigFromController returned", err) + continue + } + + err = applyConfig(ctx, eapiClient, configText, *maxLockAge) if err != nil { - log.Println("ERROR: pollAndConfigureDevice returned", err) + log.Println("ERROR: applyConfig returned", err) + continue } + cachedConfigHash = configHash + configCacheTime = time.Now() } } } diff --git a/controlplane/agent/internal/agent/dzclient.go b/controlplane/agent/internal/agent/dzclient.go index 430dae3a8..886b94fd9 100644 --- a/controlplane/agent/internal/agent/dzclient.go +++ b/controlplane/agent/internal/agent/dzclient.go @@ -35,6 +35,28 @@ func GetConfigFromServer(ctx context.Context, client pb.ControllerClient, localD return config, nil } +func GetConfigHashFromServer(ctx context.Context, client pb.ControllerClient, localDevicePubkey string, neighborIpMap map[string][]string, controllerTimeoutInSeconds *float64, agentVersion string, agentCommit string, agentDate string) (hash string, err error) { + ctx, cancel := context.WithTimeout(ctx, time.Duration(*controllerTimeoutInSeconds*float64(time.Second))) + defer cancel() + + var bgpPeers []string + bgpPeersByVrf := make(map[string]*pb.BgpPeers) + for vrf, peers := range neighborIpMap { + bgpPeersByVrf[vrf] = &pb.BgpPeers{Peers: peers} + bgpPeers = append(bgpPeers, peers...) + } + slices.Sort(bgpPeers) + + req := &pb.ConfigRequest{Pubkey: localDevicePubkey, BgpPeers: bgpPeers, BgpPeersByVrf: bgpPeersByVrf, AgentVersion: &agentVersion, AgentCommit: &agentCommit, AgentDate: &agentDate} + resp, err := client.GetConfigHash(ctx, req) + if err != nil { + log.Printf("Error calling GetConfigHash: %v\n", err) + return "", err + } + + return resp.GetHash(), nil +} + func GetDzClient(controllerAddressAndPort string) (pb.ControllerClient, error) { conn, err := grpc.NewClient(controllerAddressAndPort, grpc.WithTransportCredentials(insecure.NewCredentials())) log.Printf("controllerAddressAndPort %s\n", controllerAddressAndPort) diff --git a/controlplane/controller/README.md b/controlplane/controller/README.md index 27dbc7b87..83ba6740c 100644 --- a/controlplane/controller/README.md +++ b/controlplane/controller/README.md @@ -2,6 +2,92 @@ The controller generates device configurations from Solana smart contract state and serves them to agents running on network devices via gRPC. +## Architecture + +### Agent-Controller Communication Flow + +The controller provides two gRPC endpoints, GetConfig and GetConfigHash, that agents use to detect and apply configuration changes. Here's how the agent uses the endpoints. + +``` +┌─────────┐ ┌────────────┐ ┌────────────┐ ┌─────────┐ +│ Agent │ │ Controller │ │ Controller │ │ EOS │ +│ main() │ │GetConfigHash │ Config │ │ Device │ +│ │ │ GetConfig()│ │ Generator │ │ │ +└────┬────┘ └─────┬──────┘ └─────┬──────┘ └────┬────┘ + │ │ │ │ + │ Every 5s: │ │ │ + │ │ │ │ + │ GetBgpNeighbors() │ │ │ + ├─────────────────────────────────────────────────────────────────────────────────────────►│ + │◄─────────────────────────────────────────────────────────────────────────────────────────┤ + │ [peer IPs] │ │ │ + │ │ │ │ + │ Decision: should fetch? │ │ │ + │ • First run (no hash)? │ │ │ + │ • 1m since last apply? │ │ │ + │ • Hash changed? │ │ │ + │ │ │ │ + │ GetConfigHashFromServer() │ │ │ + ├───────────────────────────►│ │ │ + │ │ processConfigRequest() │ │ + │ ├─────────────────────────────►│ │ + │ │ │ generateConfig() │ + │ │ │ • deduplicateTunnels() │ + │ │ │ • renderConfig() │ + │ │ │ SHA256(config) │ + │ │◄─────────────────────────────┤ │ + │ │ [hash only] │ │ + │◄───────────────────────────┤ │ │ + │ ConfigHashResponse │ │ │ + │ {hash: "abc123..."} │ │ │ + │ (64 bytes) │ │ │ + │ │ │ │ + │ Compare: hash != lastHash? │ │ │ + │ │ │ │ + ├─── if YES (or first run or 5m timeout): │ + │ │ │ │ + │ fetchConfigFromController() │ │ + │ ├─► GetConfigFromServer() │ │ + │ │ ──────────────────► │ │ │ + │ │ │ processConfigRequest() │ │ + │ │ ├─────────────────────────────►│ │ + │ │ │ │ generateConfig() │ + │ │ │ │ • deduplicateTunnels() │ + │ │ │ │ • renderConfig() │ + │ │ │ │ (entire config text) │ + │ │ │◄─────────────────────────────┤ │ + │ │ ◄──────────────────│ [config string] │ │ + │ │ ConfigResponse │ │ │ + │ │ {config: "..."} │ │ │ + │ │ │ │ │ + │ ├─► computeChecksum(config) │ │ + │ │ [local SHA256] │ │ │ + │ │ │ │ │ + │ └─► return config+hash │ │ │ + │ │ │ │ + │ applyConfig() │ │ │ + │ └─► AddConfigToDevice(config) │ │ + │ ─────────────────────────────────────────────────────────────────────────────────►│ + │ ◄─────────────────────────────────────────────────────────────────────────────────┤ + │ [config applied] │ │ │ + │ │ │ │ + │ lastChecksum = hash │ │ │ + │ lastApplyTime = now │ │ │ + │ │ │ │ + ├─── else: skip this cycle (hash unchanged, no work needed) | │ + │ │ │ │ + │ sleep(5s) │ │ │ + │ goto top │ │ │ + │ │ │ │ +``` + +**Key Benefits:** +- **Network**: 64 bytes vs ~50KB on most cycles (99%+ reduction when config unchanged) +- **CPU**: Config generation still happens on controller (for hash), but EOS device skips apply +- **Safety**: Full config check every 5 minutes (300s) as fallback +- **Responsiveness**: Still checks for changes every 5 seconds +- **Decision points**: First run, 5m timeout, or hash mismatch triggers full fetch + ## Configuration ### ClickHouse Integration diff --git a/controlplane/controller/internal/controller/metrics.go b/controlplane/controller/internal/controller/metrics.go index 706a9b838..ed909a854 100644 --- a/controlplane/controller/internal/controller/metrics.go +++ b/controlplane/controller/internal/controller/metrics.go @@ -43,6 +43,13 @@ var ( []string{"pubkey", "device_code", "contributor_code", "exchange_code", "location_code", "device_status", "agent_version", "agent_commit", "agent_date"}, ) + getConfigHashOps = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "controller_grpc_getconfighash_requests_total", + Help: "The total number of getconfighash requests", + }, + []string{"pubkey", "device_code", "contributor_code", "exchange_code", "location_code", "device_status", "agent_version", "agent_commit", "agent_date"}, + ) + getConfigMsgSize = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "controller_grpc_getconfig_msg_size_bytes", Help: "The size of GetConfig response messages in bytes", @@ -101,6 +108,7 @@ func init() { prometheus.MustRegister(getConfigRenderErrors) prometheus.MustRegister(duplicateTunnelPairs) prometheus.MustRegister(getConfigOps) + prometheus.MustRegister(getConfigHashOps) prometheus.MustRegister(getConfigMsgSize) prometheus.MustRegister(getConfigDuration) diff --git a/controlplane/controller/internal/controller/server.go b/controlplane/controller/internal/controller/server.go index ce3fd38d2..36d140846 100644 --- a/controlplane/controller/internal/controller/server.go +++ b/controlplane/controller/internal/controller/server.go @@ -2,7 +2,9 @@ package controller import ( "context" + "crypto/sha256" "crypto/tls" + "encoding/hex" "errors" "fmt" "log/slog" @@ -683,78 +685,13 @@ func (c *Controller) deduplicateTunnels(device *Device) []*Tunnel { return unique } -// GetConfig renders the latest device configuration based on cached device data -func (c *Controller) GetConfig(ctx context.Context, req *pb.ConfigRequest) (*pb.ConfigResponse, error) { - reqStart := time.Now() - c.mu.RLock() - defer c.mu.RUnlock() - device, ok := c.cache.Devices[req.GetPubkey()] - if !ok { - getConfigPubkeyErrors.WithLabelValues(req.GetPubkey()).Inc() - err := status.Errorf(codes.NotFound, "pubkey %s not found", req.Pubkey) - return nil, err - } - if len(device.DevicePathologies) > 0 { - err := status.Errorf(codes.FailedPrecondition, "cannot render config for device %s: %v", req.Pubkey, device.DevicePathologies) - return nil, err - } - +// generateConfig renders the device configuration. It must be called with c.mu held +// because it reads from c.cache, which is updated by a background goroutine. +func (c *Controller) generateConfig(pubkey string, device *Device, unknownPeers []net.IP) (string, error) { // Create shallow copy of device with deduplicated tunnels deviceForRender := *device deviceForRender.Tunnels = c.deduplicateTunnels(device) - agentVersion := req.GetAgentVersion() - agentCommit := req.GetAgentCommit() - agentDate := req.GetAgentDate() - - // Record metrics with device labels - getConfigOps.WithLabelValues( - req.GetPubkey(), - device.Code, - device.ContributorCode, - device.ExchangeCode, - device.LocationCode, - device.Status.String(), - agentVersion, - agentCommit, - agentDate, - ).Inc() - - // compare peers from device to on-chain - peerFound := func(peer net.IP) bool { - for _, tun := range deviceForRender.Tunnels { - if tun.OverlayDstIP.Equal(peer) { - return true - } - } - for _, bgpPeer := range c.cache.Vpnv4BgpPeers { // TODO: write a test that proves we don't remove ipv4/vpnv4 BGP peers - if bgpPeer.PeerIP.Equal(peer) { - return true - } - } - for _, bgpPeer := range c.cache.Ipv4BgpPeers { - if bgpPeer.PeerIP.Equal(peer) { - return true - } - } - return false - } - - unknownPeers := []net.IP{} - for _, peer := range req.GetBgpPeers() { - ip := net.ParseIP(peer) - if ip == nil { - continue - } - if peerFound(ip) { - continue - } - // Only remove peers with addresses that DZ has assigned. This will avoid removal of contributor-configured peers like DIA. - if isIPInBlock(ip, c.cache.Config.UserTunnelBlock) || isIPInBlock(ip, c.cache.Config.TunnelTunnelBlock) { - unknownPeers = append(unknownPeers, ip) - } - } - multicastGroupBlock := formatCIDR(&c.cache.Config.MulticastGroupBlock) // This check avoids the situation where the template produces the following useless output, which happens in any test case with a single DZD. @@ -769,21 +706,17 @@ func (c *Controller) GetConfig(ctx context.Context, req *pb.ConfigRequest) (*pb. var localASN uint32 if c.deviceLocalASN != 0 { - // Use the explicitly provided ASN localASN = c.deviceLocalASN } else if c.environment != "" { - // Get ASN from environment networkConfig, err := config.NetworkConfigForEnv(c.environment) if err != nil { - getConfigRenderErrors.WithLabelValues(req.GetPubkey()).Inc() - err := status.Errorf(codes.Internal, "failed to get network config for environment %s: %v", c.environment, err) - return nil, err + getConfigRenderErrors.WithLabelValues(pubkey).Inc() + return "", status.Errorf(codes.Internal, "failed to get network config for environment %s: %v", c.environment, err) } localASN = networkConfig.DeviceLocalASN } else { - getConfigRenderErrors.WithLabelValues(req.GetPubkey()).Inc() - err := status.Errorf(codes.Internal, "device local ASN not configured") - return nil, err + getConfigRenderErrors.WithLabelValues(pubkey).Inc() + return "", status.Errorf(codes.Internal, "device local ASN not configured") } data := templateData{ @@ -799,13 +732,94 @@ func (c *Controller) GetConfig(ctx context.Context, req *pb.ConfigRequest) (*pb. Strings: StringsHelper{}, } - config, err := renderConfig(data) + configStr, err := renderConfig(data) + if err != nil { + getConfigRenderErrors.WithLabelValues(pubkey).Inc() + return "", status.Errorf(codes.Aborted, "config rendering for pubkey %s failed: %v", pubkey, err) + } + + return configStr, nil +} + +// processConfigRequest validates the request and generates the config. +// It must be called with c.mu held. +// Returns the config string and the device (for metric labeling by the caller). +func (c *Controller) processConfigRequest(req *pb.ConfigRequest) (string, *Device, error) { + pubkey := req.GetPubkey() + device, ok := c.cache.Devices[pubkey] + if !ok { + getConfigPubkeyErrors.WithLabelValues(pubkey).Inc() + return "", nil, status.Errorf(codes.NotFound, "pubkey %s not found", pubkey) + } + if len(device.DevicePathologies) > 0 { + return "", nil, status.Errorf(codes.FailedPrecondition, "cannot render config for device %s: %v", pubkey, device.DevicePathologies) + } + + // Find unknown BGP peers that need to be removed + peerFound := func(peer net.IP) bool { + for _, tun := range device.Tunnels { + if tun.OverlayDstIP.Equal(peer) { + return true + } + } + for _, bgpPeer := range c.cache.Vpnv4BgpPeers { + if bgpPeer.PeerIP.Equal(peer) { + return true + } + } + for _, bgpPeer := range c.cache.Ipv4BgpPeers { + if bgpPeer.PeerIP.Equal(peer) { + return true + } + } + return false + } + + var unknownPeers []net.IP + for _, peer := range req.GetBgpPeers() { + ip := net.ParseIP(peer) + if ip == nil { + continue + } + if peerFound(ip) { + continue + } + if isIPInBlock(ip, c.cache.Config.UserTunnelBlock) || isIPInBlock(ip, c.cache.Config.TunnelTunnelBlock) { + unknownPeers = append(unknownPeers, ip) + } + } + + configStr, err := c.generateConfig(pubkey, device, unknownPeers) + if err != nil { + return "", nil, err + } + return configStr, device, nil +} + +// GetConfig renders the latest device configuration based on cached device data +func (c *Controller) GetConfig(ctx context.Context, req *pb.ConfigRequest) (*pb.ConfigResponse, error) { + reqStart := time.Now() + c.mu.RLock() + defer c.mu.RUnlock() + + configStr, device, err := c.processConfigRequest(req) if err != nil { - getConfigRenderErrors.WithLabelValues(req.GetPubkey()).Inc() - err := status.Errorf(codes.Aborted, "config rendering for pubkey %s failed: %v", req.Pubkey, err) return nil, err } - resp := &pb.ConfigResponse{Config: config} + + getConfigOps.WithLabelValues( + req.GetPubkey(), + device.Code, + device.ContributorCode, + device.ExchangeCode, + device.LocationCode, + device.Status.String(), + req.GetAgentVersion(), + req.GetAgentCommit(), + req.GetAgentDate(), + ).Inc() + + resp := &pb.ConfigResponse{Config: configStr} getConfigMsgSize.Observe(float64(proto.Size(resp))) getConfigDuration.Observe(float64(time.Since(reqStart).Seconds())) if c.clickhouse != nil { @@ -817,6 +831,34 @@ func (c *Controller) GetConfig(ctx context.Context, req *pb.ConfigRequest) (*pb. return resp, nil } +// GetConfigHash returns only the hash of the configuration for change detection +func (c *Controller) GetConfigHash(ctx context.Context, req *pb.ConfigRequest) (*pb.ConfigHashResponse, error) { + reqStart := time.Now() + c.mu.RLock() + defer c.mu.RUnlock() + + configStr, device, err := c.processConfigRequest(req) + if err != nil { + return nil, err + } + + getConfigHashOps.WithLabelValues( + req.GetPubkey(), + device.Code, + device.ContributorCode, + device.ExchangeCode, + device.LocationCode, + device.Status.String(), + req.GetAgentVersion(), + req.GetAgentCommit(), + req.GetAgentDate(), + ).Inc() + + hash := sha256.Sum256([]byte(configStr)) + getConfigDuration.Observe(float64(time.Since(reqStart).Seconds())) + return &pb.ConfigHashResponse{Hash: hex.EncodeToString(hash[:])}, nil +} + // formatCIDR formats a 5-byte network block into CIDR notation func formatCIDR(b *[5]byte) string { ip := net.IPv4(b[0], b[1], b[2], b[3]) diff --git a/controlplane/proto/controller/controller.proto b/controlplane/proto/controller/controller.proto index 8298e8a81..08f4cd189 100644 --- a/controlplane/proto/controller/controller.proto +++ b/controlplane/proto/controller/controller.proto @@ -8,6 +8,8 @@ option go_package = "github.com/malbeclabs/doublezero/controlplane/proto/control service Controller { // Returns the latest configuration of a DoubleZero node based on on-chain data rpc GetConfig (ConfigRequest) returns (ConfigResponse) {} + // Returns only the hash of the latest configuration (lightweight check for changes) + rpc GetConfigHash (ConfigRequest) returns (ConfigHashResponse) {} } // Request for latest configuration of a DoubleZero node based on its public key @@ -30,4 +32,9 @@ message BgpPeers { message ConfigResponse { string config = 1; string hash = 2; +} + +// Response containing only the config hash +message ConfigHashResponse { + string hash = 1; } \ No newline at end of file diff --git a/controlplane/proto/controller/gen/pb-go/controller.pb.go b/controlplane/proto/controller/gen/pb-go/controller.pb.go index d9813c885..ec4862417 100644 --- a/controlplane/proto/controller/gen/pb-go/controller.pb.go +++ b/controlplane/proto/controller/gen/pb-go/controller.pb.go @@ -220,6 +220,54 @@ func (x *ConfigResponse) GetHash() string { return "" } +// Response containing only the config hash +type ConfigHashResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` +} + +func (x *ConfigHashResponse) Reset() { + *x = ConfigHashResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfigHashResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigHashResponse) ProtoMessage() {} + +func (x *ConfigHashResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigHashResponse.ProtoReflect.Descriptor instead. +func (*ConfigHashResponse) Descriptor() ([]byte, []int) { + return file_controller_proto_rawDescGZIP(), []int{3} +} + +func (x *ConfigHashResponse) GetHash() string { + if x != nil { + return x.Hash + } + return "" +} + var File_controller_proto protoreflect.FileDescriptor var file_controller_proto_rawDesc = []byte{ @@ -258,16 +306,24 @@ var file_controller_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, - 0x32, 0x52, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x12, 0x44, - 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6d, 0x61, 0x6c, 0x62, 0x65, 0x63, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x64, 0x6f, - 0x75, 0x62, 0x6c, 0x65, 0x7a, 0x65, 0x72, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x28, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x61, 0x73, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x32, 0xa0, 0x01, 0x0a, 0x0a, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x12, 0x44, 0x0a, 0x09, 0x47, 0x65, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x4c, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x61, 0x73, 0x68, + 0x12, 0x19, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, + 0x61, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x40, 0x5a, + 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x61, 0x6c, 0x62, + 0x65, 0x63, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x7a, 0x65, 0x72, + 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -282,20 +338,23 @@ func file_controller_proto_rawDescGZIP() []byte { return file_controller_proto_rawDescData } -var file_controller_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_controller_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_controller_proto_goTypes = []interface{}{ - (*ConfigRequest)(nil), // 0: controller.ConfigRequest - (*BgpPeers)(nil), // 1: controller.BgpPeers - (*ConfigResponse)(nil), // 2: controller.ConfigResponse - nil, // 3: controller.ConfigRequest.BgpPeersByVrfEntry + (*ConfigRequest)(nil), // 0: controller.ConfigRequest + (*BgpPeers)(nil), // 1: controller.BgpPeers + (*ConfigResponse)(nil), // 2: controller.ConfigResponse + (*ConfigHashResponse)(nil), // 3: controller.ConfigHashResponse + nil, // 4: controller.ConfigRequest.BgpPeersByVrfEntry } var file_controller_proto_depIdxs = []int32{ - 3, // 0: controller.ConfigRequest.bgp_peers_by_vrf:type_name -> controller.ConfigRequest.BgpPeersByVrfEntry + 4, // 0: controller.ConfigRequest.bgp_peers_by_vrf:type_name -> controller.ConfigRequest.BgpPeersByVrfEntry 1, // 1: controller.ConfigRequest.BgpPeersByVrfEntry.value:type_name -> controller.BgpPeers 0, // 2: controller.Controller.GetConfig:input_type -> controller.ConfigRequest - 2, // 3: controller.Controller.GetConfig:output_type -> controller.ConfigResponse - 3, // [3:4] is the sub-list for method output_type - 2, // [2:3] is the sub-list for method input_type + 0, // 3: controller.Controller.GetConfigHash:input_type -> controller.ConfigRequest + 2, // 4: controller.Controller.GetConfig:output_type -> controller.ConfigResponse + 3, // 5: controller.Controller.GetConfigHash:output_type -> controller.ConfigHashResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name @@ -343,6 +402,18 @@ func file_controller_proto_init() { return nil } } + file_controller_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfigHashResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_proto_msgTypes[0].OneofWrappers = []interface{}{} type x struct{} @@ -351,7 +422,7 @@ func file_controller_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/controlplane/proto/controller/gen/pb-go/controller_grpc.pb.go b/controlplane/proto/controller/gen/pb-go/controller_grpc.pb.go index c41e285ae..fde73232e 100644 --- a/controlplane/proto/controller/gen/pb-go/controller_grpc.pb.go +++ b/controlplane/proto/controller/gen/pb-go/controller_grpc.pb.go @@ -24,6 +24,8 @@ const _ = grpc.SupportPackageIsVersion7 type ControllerClient interface { // Returns the latest configuration of a DoubleZero node based on on-chain data GetConfig(ctx context.Context, in *ConfigRequest, opts ...grpc.CallOption) (*ConfigResponse, error) + // Returns only the hash of the latest configuration (lightweight check for changes) + GetConfigHash(ctx context.Context, in *ConfigRequest, opts ...grpc.CallOption) (*ConfigHashResponse, error) } type controllerClient struct { @@ -43,12 +45,23 @@ func (c *controllerClient) GetConfig(ctx context.Context, in *ConfigRequest, opt return out, nil } +func (c *controllerClient) GetConfigHash(ctx context.Context, in *ConfigRequest, opts ...grpc.CallOption) (*ConfigHashResponse, error) { + out := new(ConfigHashResponse) + err := c.cc.Invoke(ctx, "/controller.Controller/GetConfigHash", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ControllerServer is the server API for Controller service. // All implementations should embed UnimplementedControllerServer // for forward compatibility type ControllerServer interface { // Returns the latest configuration of a DoubleZero node based on on-chain data GetConfig(context.Context, *ConfigRequest) (*ConfigResponse, error) + // Returns only the hash of the latest configuration (lightweight check for changes) + GetConfigHash(context.Context, *ConfigRequest) (*ConfigHashResponse, error) } // UnimplementedControllerServer should be embedded to have forward compatible implementations. @@ -58,6 +71,9 @@ type UnimplementedControllerServer struct { func (UnimplementedControllerServer) GetConfig(context.Context, *ConfigRequest) (*ConfigResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") } +func (UnimplementedControllerServer) GetConfigHash(context.Context, *ConfigRequest) (*ConfigHashResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetConfigHash not implemented") +} // UnsafeControllerServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ControllerServer will @@ -88,6 +104,24 @@ func _Controller_GetConfig_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _Controller_GetConfigHash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ControllerServer).GetConfigHash(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.Controller/GetConfigHash", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ControllerServer).GetConfigHash(ctx, req.(*ConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Controller_ServiceDesc is the grpc.ServiceDesc for Controller service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -99,6 +133,10 @@ var Controller_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetConfig", Handler: _Controller_GetConfig_Handler, }, + { + MethodName: "GetConfigHash", + Handler: _Controller_GetConfigHash_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "controller.proto",