Add Cloud Map seeder discovery with VPC Lattice operator templates.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Agents poll AETHERFORGE_CLOUD_MAP_ENDPOINT to sync know_node gossip; deploy plans attach route_via DNS hints for standalone operator registries.
This commit is contained in:
@@ -430,6 +430,7 @@ func (c *AgentClient) authenticate() error {
|
||||
c.startContingencyIfEnabled(c.miningCtx)
|
||||
c.applyAuthFleetRole(resp)
|
||||
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
|
||||
c.startCloudMapSync()
|
||||
if resp.FleetTorrentEnabled {
|
||||
c.advertiseFleetTorrentHealthy()
|
||||
}
|
||||
|
||||
41
agent/client/cloud_map.go
Normal file
41
agent/client/cloud_map.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
const cloudMapSyncInterval = 5 * time.Minute
|
||||
|
||||
func (c *AgentClient) startCloudMapSync() {
|
||||
endpoint := deploy.CloudMapEndpoint()
|
||||
if endpoint == "" {
|
||||
return
|
||||
}
|
||||
go c.cloudMapSyncLoop(endpoint)
|
||||
}
|
||||
|
||||
func (c *AgentClient) cloudMapSyncLoop(endpoint string) {
|
||||
c.syncCloudMapOnce(endpoint)
|
||||
ticker := time.NewTicker(cloudMapSyncInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.miningCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.syncCloudMapOnce(endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) syncCloudMapOnce(endpoint string) {
|
||||
err := deploy.SyncCloudMapRegistry(endpoint, c.writeFleetTorrentGossip)
|
||||
if err != nil {
|
||||
log.Printf("[agent] cloud map registry sync failed: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[agent] cloud map registry synced from %s", endpoint)
|
||||
}
|
||||
157
agent/deploy/cloud_map.go
Normal file
157
agent/deploy/cloud_map.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const cloudMapEnvEndpoint = "AETHERFORGE_CLOUD_MAP_ENDPOINT"
|
||||
|
||||
type CloudMapRegistryDocument struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Service string `json:"service"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Instances []CloudMapRegistryInstance `json:"instances"`
|
||||
}
|
||||
|
||||
type CloudMapRegistryInstance struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
DNSName string `json:"dns_name"`
|
||||
IP string `json:"ip"`
|
||||
FetchURL string `json:"fetch_url"`
|
||||
Port int `json:"port"`
|
||||
Healthy bool `json:"healthy"`
|
||||
}
|
||||
|
||||
var cloudMapHTTPGet = func(url string) ([]byte, error) {
|
||||
client := &http.Client{Timeout: 12 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("cloud map registry HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
}
|
||||
|
||||
func SetCloudMapHTTPGetForTest(fn func(string) ([]byte, error)) {
|
||||
if fn == nil {
|
||||
cloudMapHTTPGet = func(url string) ([]byte, error) {
|
||||
client := &http.Client{Timeout: 12 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("cloud map registry HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
}
|
||||
return
|
||||
}
|
||||
cloudMapHTTPGet = fn
|
||||
}
|
||||
|
||||
func CloudMapEndpoint() string {
|
||||
return strings.TrimRight(strings.TrimSpace(os.Getenv(cloudMapEnvEndpoint)), "/")
|
||||
}
|
||||
|
||||
func normalizeCloudMapRegistry(doc CloudMapRegistryDocument) CloudMapRegistryDocument {
|
||||
doc.Namespace = strings.TrimSpace(doc.Namespace)
|
||||
doc.Service = strings.TrimSpace(doc.Service)
|
||||
if doc.Namespace == "" {
|
||||
doc.Namespace = "prod.local"
|
||||
}
|
||||
if doc.Service == "" {
|
||||
doc.Service = "seeder"
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func FetchCloudMapRegistry(endpoint string) (CloudMapRegistryDocument, error) {
|
||||
endpoint = strings.TrimRight(strings.TrimSpace(endpoint), "/")
|
||||
if endpoint == "" {
|
||||
return CloudMapRegistryDocument{}, fmt.Errorf("cloud map endpoint required")
|
||||
}
|
||||
body, err := cloudMapHTTPGet(endpoint)
|
||||
if err != nil {
|
||||
return CloudMapRegistryDocument{}, err
|
||||
}
|
||||
var doc CloudMapRegistryDocument
|
||||
if err := json.Unmarshal(body, &doc); err != nil {
|
||||
return CloudMapRegistryDocument{}, err
|
||||
}
|
||||
return normalizeCloudMapRegistry(doc), nil
|
||||
}
|
||||
|
||||
func CloudMapKnowNodeRecords(doc CloudMapRegistryDocument) []FleetGossipRecord {
|
||||
doc = normalizeCloudMapRegistry(doc)
|
||||
var out []FleetGossipRecord
|
||||
seen := make(map[string]bool)
|
||||
for _, inst := range doc.Instances {
|
||||
if !inst.Healthy {
|
||||
continue
|
||||
}
|
||||
target := strings.TrimSpace(inst.AgentID)
|
||||
if target == "" {
|
||||
target = strings.TrimSpace(inst.DNSName)
|
||||
}
|
||||
if target == "" || seen[target] {
|
||||
continue
|
||||
}
|
||||
seen[target] = true
|
||||
out = append(out, FleetGossipRecord{
|
||||
Kind: FleetGossipKnowNode,
|
||||
TargetAgentID: target,
|
||||
Healthy: true,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CloudMapLANSeederHints(doc CloudMapRegistryDocument) []LANSeederHint {
|
||||
doc = normalizeCloudMapRegistry(doc)
|
||||
var out []LANSeederHint
|
||||
for _, inst := range doc.Instances {
|
||||
if !inst.Healthy {
|
||||
continue
|
||||
}
|
||||
id := strings.TrimSpace(inst.AgentID)
|
||||
if id == "" {
|
||||
id = strings.TrimSpace(inst.DNSName)
|
||||
}
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, LANSeederHint{
|
||||
AgentID: id,
|
||||
IP: strings.TrimSpace(inst.IP),
|
||||
LANFallbackURL: strings.TrimSpace(inst.FetchURL),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func SyncCloudMapRegistry(endpoint string, gossipFn func([]FleetGossipRecord)) error {
|
||||
doc, err := FetchCloudMapRegistry(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
records := CloudMapKnowNodeRecords(doc)
|
||||
if len(records) > 0 && gossipFn != nil {
|
||||
gossipFn(records)
|
||||
}
|
||||
if hints := CloudMapLANSeederHints(doc); len(hints) > 0 {
|
||||
localIP, _ := PrimaryLocalIPv4()
|
||||
SetLANSeederHints(hints, localIP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
43
agent/deploy/cloud_map_test.go
Normal file
43
agent/deploy/cloud_map_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFetchCloudMapRegistry(t *testing.T) {
|
||||
SetCloudMapHTTPGetForTest(func(url string) ([]byte, error) {
|
||||
return []byte(`{"namespace":"prod.local","service":"seeder","instances":[{"agent_id":"seed-a","dns_name":"seeder.svc.prod.local","healthy":true,"fetch_url":"http://10.0.1.50/manifest"}]}`), nil
|
||||
})
|
||||
defer SetCloudMapHTTPGetForTest(nil)
|
||||
doc, err := FetchCloudMapRegistry("http://registry.local/v1/seeder")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
records := CloudMapKnowNodeRecords(doc)
|
||||
if len(records) != 1 || records[0].TargetAgentID != "seed-a" {
|
||||
t.Fatalf("records=%+v", records)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendSpreadRouteTelemetryIncludesRouteVia(t *testing.T) {
|
||||
got := appendSpreadRouteTelemetry("joined", &SpreadRouteHint{EgressAgentID: "egress-1", RouteVia: "seeder.svc.prod.local"})
|
||||
if got == "" || !strings.Contains(got, "route_via=seeder.svc.prod.local") {
|
||||
t.Fatalf("got=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudMapEndpointFromEnv(t *testing.T) {
|
||||
t.Setenv(cloudMapEnvEndpoint, "http://127.0.0.1:8080/registry.json")
|
||||
if got := CloudMapEndpoint(); got != "http://127.0.0.1:8080/registry.json" {
|
||||
t.Fatalf("got=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudMapEndpointUnset(t *testing.T) {
|
||||
_ = os.Unsetenv(cloudMapEnvEndpoint)
|
||||
if got := CloudMapEndpoint(); got != "" {
|
||||
t.Fatalf("got=%q", got)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package deploy
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
@@ -25,6 +25,7 @@ type SpreadRouteHint struct {
|
||||
ClearanceLevel int `json:"clearance_level,omitempty"`
|
||||
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
||||
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
||||
RouteVia string `json:"route_via,omitempty"`
|
||||
}
|
||||
|
||||
// WebRTCMeshPlanBody is the signed WebRTC mesh policy attached to deploy plans.
|
||||
@@ -401,6 +402,9 @@ func appendSpreadRouteTelemetry(detail string, hint *SpreadRouteHint) string {
|
||||
strings.TrimSpace(hint.SeedAgentID),
|
||||
hint.Score,
|
||||
)
|
||||
if via := strings.TrimSpace(hint.RouteVia); via != "" {
|
||||
routeNote += "; route_via=" + via
|
||||
}
|
||||
if detail == "" {
|
||||
return routeNote
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user