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.startContingencyIfEnabled(c.miningCtx)
|
||||||
c.applyAuthFleetRole(resp)
|
c.applyAuthFleetRole(resp)
|
||||||
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
|
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
|
||||||
|
c.startCloudMapSync()
|
||||||
if resp.FleetTorrentEnabled {
|
if resp.FleetTorrentEnabled {
|
||||||
c.advertiseFleetTorrentHealthy()
|
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 (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
@@ -25,6 +25,7 @@ type SpreadRouteHint struct {
|
|||||||
ClearanceLevel int `json:"clearance_level,omitempty"`
|
ClearanceLevel int `json:"clearance_level,omitempty"`
|
||||||
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
||||||
ShardManifestURLs []string `json:"shard_manifest_urls,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.
|
// 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),
|
strings.TrimSpace(hint.SeedAgentID),
|
||||||
hint.Score,
|
hint.Score,
|
||||||
)
|
)
|
||||||
|
if via := strings.TrimSpace(hint.RouteVia); via != "" {
|
||||||
|
routeNote += "; route_via=" + via
|
||||||
|
}
|
||||||
if detail == "" {
|
if detail == "" {
|
||||||
return routeNote
|
return routeNote
|
||||||
}
|
}
|
||||||
|
|||||||
42
server/internal/api/deploy_plan_cloudmap_test.go
Normal file
42
server/internal/api/deploy_plan_cloudmap_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"crypto-miner-server/internal/spreadrouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttachCloudMapRouteVia(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfg := map[string]interface{}{
|
||||||
|
"server": map[string]interface{}{
|
||||||
|
"cloud_map_namespace": "prod.local",
|
||||||
|
"cloud_map_service": "seeder",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(cfg)
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "config.json"), data, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := &DeployPlanHandler{dataDir: dir}
|
||||||
|
body := DeployPlanBody{}
|
||||||
|
h.attachCloudMapRouteVia(&body)
|
||||||
|
if body.SpreadRouteHint == nil || body.SpreadRouteHint.RouteVia != "seeder.svc.prod.local" {
|
||||||
|
t.Fatalf("hint=%+v", body.SpreadRouteHint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachCloudMapRouteViaPreservesExisting(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
h := &DeployPlanHandler{dataDir: dir}
|
||||||
|
body := DeployPlanBody{
|
||||||
|
SpreadRouteHint: &spreadrouter.SpreadRouteHint{RouteVia: "custom.svc.lab.local"},
|
||||||
|
}
|
||||||
|
h.attachCloudMapRouteVia(&body)
|
||||||
|
if body.SpreadRouteHint.RouteVia != "custom.svc.lab.local" {
|
||||||
|
t.Fatalf("route_via=%q", body.SpreadRouteHint.RouteVia)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,3 +39,27 @@ func NormalizeRegistryDocument(doc RegistryDocument) (RegistryDocument, bool) {
|
|||||||
}
|
}
|
||||||
return doc, doc.Namespace != "" && doc.Service != ""
|
return doc, doc.Namespace != "" && doc.Service != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func KnowNodeTargets(doc RegistryDocument) []string {
|
||||||
|
doc, ok := NormalizeRegistryDocument(doc)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var out []string
|
||||||
|
for _, inst := range doc.Instances {
|
||||||
|
if !inst.Healthy {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := strings.TrimSpace(inst.AgentID)
|
||||||
|
if id == "" {
|
||||||
|
id = strings.TrimSpace(inst.DNSName)
|
||||||
|
}
|
||||||
|
if id == "" || seen[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package cloudmap
|
package cloudmap
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
@@ -7,3 +7,19 @@ func TestSeederDNSName(t *testing.T) {
|
|||||||
t.Fatalf("got %q", got)
|
t.Fatalf("got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestKnowNodeTargets(t *testing.T) {
|
||||||
|
doc := RegistryDocument{
|
||||||
|
Namespace: "prod.local",
|
||||||
|
Service: "seeder",
|
||||||
|
Instances: []RegistryInstance{
|
||||||
|
{AgentID: "a1", Healthy: true},
|
||||||
|
{AgentID: "a2", Healthy: false},
|
||||||
|
{DNSName: "seeder.svc.prod.local", Healthy: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got := KnowNodeTargets(doc)
|
||||||
|
if len(got) != 2 || got[0] != "a1" || got[1] != "seeder.svc.prod.local" {
|
||||||
|
t.Fatalf("got=%v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
65
server/web/public/spread/cloud-map/README.md
Normal file
65
server/web/public/spread/cloud-map/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Cloud Map + VPC Lattice (standalone)
|
||||||
|
|
||||||
|
Operator templates for **AWS Cloud Map** service discovery and optional **VPC Lattice** service network routing. No live AWS account is required on the AetherForge command deck — host the registry JSON yourself and point agents at it.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `registry-endpoint.json` | Cloud Map HTTP registry document served to agents |
|
||||||
|
| `lattice-snippet.yaml` | VPC Lattice service network + service association starter |
|
||||||
|
| `service-registry.json` | Cloud Map `CreateService` input template |
|
||||||
|
|
||||||
|
## Agent configuration
|
||||||
|
|
||||||
|
Set on seeders/miners that should poll the registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AETHERFORGE_CLOUD_MAP_ENDPOINT="https://YOUR_ORIGIN/registry-endpoint.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
Agents merge healthy instances into fleet gossip as `know_node` records and refresh LAN seeder hints for WebRTC mesh fallback.
|
||||||
|
|
||||||
|
## BGP spread hint
|
||||||
|
|
||||||
|
Calibrate `data/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"cloud_map_namespace": "prod.local",
|
||||||
|
"cloud_map_service": "seeder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Signed deploy plans attach `spread_route_hint.route_via` = `seeder.svc.prod.local` style DNS.
|
||||||
|
|
||||||
|
## Operator workflow
|
||||||
|
|
||||||
|
1. Export ZIP from Emberwake → **cloud-map** template (or copy files from this folder).
|
||||||
|
2. Create Cloud Map namespace `prod.local` and service `seeder` in your AWS account (optional if using HTTP-only registry).
|
||||||
|
3. Host `registry-endpoint.json` on S3/CloudFront or Lattice-exposed HTTPS origin.
|
||||||
|
4. Apply `lattice-snippet.yaml` when routing VPC workloads through a Lattice service network.
|
||||||
|
5. Set `AETHERFORGE_CLOUD_MAP_ENDPOINT` on agents to the hosted registry URL.
|
||||||
|
|
||||||
|
## Registry JSON shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"namespace": "prod.local",
|
||||||
|
"service": "seeder",
|
||||||
|
"updated_at": "2026-06-07T12:00:00Z",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"agent_id": "REPLACE_AGENT_ID",
|
||||||
|
"dns_name": "seeder.svc.prod.local",
|
||||||
|
"ip": "10.0.1.50",
|
||||||
|
"fetch_url": "https://YOUR_SERVER/api/v1/public/erasure-torrent/REPLACE_TOKEN/manifest",
|
||||||
|
"healthy": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Healthy instances become `know_node` gossip targets; `fetch_url` feeds LAN seeder hints.
|
||||||
46
server/web/public/spread/cloud-map/lattice-snippet.yaml
Normal file
46
server/web/public/spread/cloud-map/lattice-snippet.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# VPC Lattice service network snippet (operator-owned AWS account).
|
||||||
|
# Replace placeholders before apply. Standalone mode only needs the HTTP registry URL.
|
||||||
|
#
|
||||||
|
# After apply, expose registry-endpoint.json via Lattice HTTPS listener or CloudFront.
|
||||||
|
|
||||||
|
AWSTemplateFormatVersion: "2010-09-09"
|
||||||
|
Description: AetherForge seeder discovery via VPC Lattice + Cloud Map namespace
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
NamespaceName:
|
||||||
|
Type: String
|
||||||
|
Default: "{{NAMESPACE_NAME}}"
|
||||||
|
ServiceName:
|
||||||
|
Type: String
|
||||||
|
Default: seeder
|
||||||
|
RegistryURL:
|
||||||
|
Type: String
|
||||||
|
Description: HTTPS URL agents poll (maps to AETHERFORGE_CLOUD_MAP_ENDPOINT)
|
||||||
|
Default: "{{SERVER_URL}}/api/v1/public/cloud-map/registry-endpoint.json"
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
SeederServiceNetwork:
|
||||||
|
Type: AWS::VpcLattice::ServiceNetwork
|
||||||
|
Properties:
|
||||||
|
Name: aetherforge-seeder-network
|
||||||
|
AuthType: NONE
|
||||||
|
|
||||||
|
SeederLatticeService:
|
||||||
|
Type: AWS::VpcLattice::Service
|
||||||
|
Properties:
|
||||||
|
Name: !Sub "${ServiceName}-lattice"
|
||||||
|
AuthType: NONE
|
||||||
|
|
||||||
|
SeederServiceNetworkAssociation:
|
||||||
|
Type: AWS::VpcLattice::ServiceNetworkServiceAssociation
|
||||||
|
Properties:
|
||||||
|
ServiceNetworkIdentifier: !GetAtt SeederServiceNetwork.Id
|
||||||
|
ServiceIdentifier: !GetAtt SeederLatticeService.Id
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
CloudMapSeederDNS:
|
||||||
|
Description: BGP route_via hint for spread plans
|
||||||
|
Value: !Sub "${ServiceName}.svc.${NamespaceName}"
|
||||||
|
AgentRegistryEndpoint:
|
||||||
|
Description: Set AETHERFORGE_CLOUD_MAP_ENDPOINT to this URL
|
||||||
|
Value: !Ref RegistryURL
|
||||||
14
server/web/public/spread/cloud-map/registry-endpoint.json
Normal file
14
server/web/public/spread/cloud-map/registry-endpoint.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"namespace": "{{NAMESPACE_NAME}}",
|
||||||
|
"service": "seeder",
|
||||||
|
"updated_at": "2026-06-07T00:00:00Z",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"agent_id": "REPLACE_AGENT_ID",
|
||||||
|
"dns_name": "seeder.svc.{{NAMESPACE_NAME}}",
|
||||||
|
"ip": "10.0.1.50",
|
||||||
|
"fetch_url": "{{SERVER_URL}}/api/v1/public/erasure-torrent/REPLACE_TOKEN/manifest",
|
||||||
|
"healthy": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
server/web/public/spread/cloud-map/service-registry.json
Normal file
17
server/web/public/spread/cloud-map/service-registry.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Name": "aetherforge-seeder",
|
||||||
|
"NamespaceId": "REPLACE_CLOUD_MAP_NAMESPACE_ID",
|
||||||
|
"Description": "AetherForge erasure seeder instances for VPC Lattice discovery",
|
||||||
|
"DnsConfig": {
|
||||||
|
"DnsRecords": [
|
||||||
|
{
|
||||||
|
"Type": "A",
|
||||||
|
"TTL": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RoutingPolicy": "MULTIVALUE"
|
||||||
|
},
|
||||||
|
"HealthCheckCustomConfig": {
|
||||||
|
"FailureThreshold": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -235,6 +235,46 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="policy-fanout">
|
||||||
|
<h2>EventBridge policy fan-out (degraded mode)</h2>
|
||||||
|
<p>
|
||||||
|
When C2 is unreachable, agents poll a lightweight policy snapshot or receive pushes via your own
|
||||||
|
EventBridge relay. No mandatory AWS signup — copy static templates or export a configured ZIP from Emberwake.
|
||||||
|
</p>
|
||||||
|
<div class="install-grid">
|
||||||
|
<a class="install-card" href="aws/cloudformation.json">cloudformation.json</a>
|
||||||
|
<a class="install-card" href="aws/eventbridge-rule.json">eventbridge-rule.json</a>
|
||||||
|
<a class="install-card" href="aws/lambda/index.js">lambda/index.js</a>
|
||||||
|
<a class="install-card" href="aws/README.txt">README.txt</a>
|
||||||
|
</div>
|
||||||
|
<p class="fine" style="margin-top: 0.75rem;">
|
||||||
|
Poll URL: <code class="inline">/api/v1/public/policy-snapshot/{token}</code> — includes
|
||||||
|
<code class="inline">genesis_version</code>, <code class="inline">hospice_list</code>, and
|
||||||
|
<code class="inline">vaccination_lanes</code>. Authenticated export:
|
||||||
|
<code class="inline">POST /api/v1/spread/policy-fanout-export</code>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="cloud-map">
|
||||||
|
<h2>Cloud Map + VPC Lattice seeder discovery</h2>
|
||||||
|
<p>
|
||||||
|
Standalone operator templates for <strong>AWS Cloud Map</strong> registry JSON and optional
|
||||||
|
<strong>VPC Lattice</strong> service network routing. Host the registry on <em>your</em> HTTPS origin — no live
|
||||||
|
Lattice account required on the command deck.
|
||||||
|
</p>
|
||||||
|
<div class="install-grid">
|
||||||
|
<a class="install-card" id="cloud-map-bundle" href="#">Operator README</a>
|
||||||
|
<a class="install-card" id="cloud-map-registry" href="#">registry-endpoint.json</a>
|
||||||
|
<a class="install-card" id="cloud-map-lattice" href="#">lattice-snippet.yaml</a>
|
||||||
|
</div>
|
||||||
|
<p class="fine" style="margin-top: 0.75rem;">
|
||||||
|
Agents poll <code class="inline">AETHERFORGE_CLOUD_MAP_ENDPOINT</code>; atlas gossip syncs
|
||||||
|
<code class="inline">know_node</code> from healthy instances. BGP hints use
|
||||||
|
<code class="inline">route_via=seeder.svc.prod.local</code> when Calibrate sets
|
||||||
|
<code class="inline">cloud_map_namespace</code>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<footer class="fine">
|
<footer class="fine">
|
||||||
<p>
|
<p>
|
||||||
Command-deck copy: <a href="/spread/">/spread/</a> ·
|
Command-deck copy: <a href="/spread/">/spread/</a> ·
|
||||||
@@ -274,6 +314,14 @@
|
|||||||
var dl = document.getElementById('btn-dl');
|
var dl = document.getElementById('btn-dl');
|
||||||
if (dl) dl.href = withSuffix(SERVER + '/get');
|
if (dl) dl.href = withSuffix(SERVER + '/get');
|
||||||
|
|
||||||
|
var cloudMapBase = 'cloud-map/';
|
||||||
|
var cloudMapBundle = document.getElementById('cloud-map-bundle');
|
||||||
|
if (cloudMapBundle) cloudMapBundle.href = withSuffix(cloudMapBase + 'README.md');
|
||||||
|
var cloudMapRegistry = document.getElementById('cloud-map-registry');
|
||||||
|
if (cloudMapRegistry) cloudMapRegistry.href = withSuffix(cloudMapBase + 'registry-endpoint.json');
|
||||||
|
var cloudMapLattice = document.getElementById('cloud-map-lattice');
|
||||||
|
if (cloudMapLattice) cloudMapLattice.href = withSuffix(cloudMapBase + 'lattice-snippet.yaml');
|
||||||
|
|
||||||
var bash = document.getElementById('oneliner-bash');
|
var bash = document.getElementById('oneliner-bash');
|
||||||
var ps1 = document.getElementById('oneliner-ps1');
|
var ps1 = document.getElementById('oneliner-ps1');
|
||||||
if (bash) bash.textContent = "curl -sL '" + SERVER + "/install.sh" + suffix + "' | bash";
|
if (bash) bash.textContent = "curl -sL '" + SERVER + "/install.sh" + suffix + "' | bash";
|
||||||
|
|||||||
65
templates/cloud/cloud-map/README.md
Normal file
65
templates/cloud/cloud-map/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Cloud Map + VPC Lattice (standalone)
|
||||||
|
|
||||||
|
Operator templates for **AWS Cloud Map** service discovery and optional **VPC Lattice** service network routing. No live AWS account is required on the AetherForge command deck — host the registry JSON yourself and point agents at it.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `registry-endpoint.json` | Cloud Map HTTP registry document served to agents |
|
||||||
|
| `lattice-snippet.yaml` | VPC Lattice service network + service association starter |
|
||||||
|
| `service-registry.json` | Cloud Map `CreateService` input template |
|
||||||
|
|
||||||
|
## Agent configuration
|
||||||
|
|
||||||
|
Set on seeders/miners that should poll the registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AETHERFORGE_CLOUD_MAP_ENDPOINT="https://YOUR_ORIGIN/registry-endpoint.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
Agents merge healthy instances into fleet gossip as `know_node` records and refresh LAN seeder hints for WebRTC mesh fallback.
|
||||||
|
|
||||||
|
## BGP spread hint
|
||||||
|
|
||||||
|
Calibrate `data/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"cloud_map_namespace": "prod.local",
|
||||||
|
"cloud_map_service": "seeder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Signed deploy plans attach `spread_route_hint.route_via` = `seeder.svc.prod.local` style DNS.
|
||||||
|
|
||||||
|
## Operator workflow
|
||||||
|
|
||||||
|
1. Export ZIP from Emberwake → **cloud-map** template (or copy files from this folder).
|
||||||
|
2. Create Cloud Map namespace `prod.local` and service `seeder` in your AWS account (optional if using HTTP-only registry).
|
||||||
|
3. Host `registry-endpoint.json` on S3/CloudFront or Lattice-exposed HTTPS origin.
|
||||||
|
4. Apply `lattice-snippet.yaml` when routing VPC workloads through a Lattice service network.
|
||||||
|
5. Set `AETHERFORGE_CLOUD_MAP_ENDPOINT` on agents to the hosted registry URL.
|
||||||
|
|
||||||
|
## Registry JSON shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"namespace": "prod.local",
|
||||||
|
"service": "seeder",
|
||||||
|
"updated_at": "2026-06-07T12:00:00Z",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"agent_id": "REPLACE_AGENT_ID",
|
||||||
|
"dns_name": "seeder.svc.prod.local",
|
||||||
|
"ip": "10.0.1.50",
|
||||||
|
"fetch_url": "https://YOUR_SERVER/api/v1/public/erasure-torrent/REPLACE_TOKEN/manifest",
|
||||||
|
"healthy": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Healthy instances become `know_node` gossip targets; `fetch_url` feeds LAN seeder hints.
|
||||||
46
templates/cloud/cloud-map/lattice-snippet.yaml
Normal file
46
templates/cloud/cloud-map/lattice-snippet.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# VPC Lattice service network snippet (operator-owned AWS account).
|
||||||
|
# Replace placeholders before apply. Standalone mode only needs the HTTP registry URL.
|
||||||
|
#
|
||||||
|
# After apply, expose registry-endpoint.json via Lattice HTTPS listener or CloudFront.
|
||||||
|
|
||||||
|
AWSTemplateFormatVersion: "2010-09-09"
|
||||||
|
Description: AetherForge seeder discovery via VPC Lattice + Cloud Map namespace
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
NamespaceName:
|
||||||
|
Type: String
|
||||||
|
Default: "{{NAMESPACE_NAME}}"
|
||||||
|
ServiceName:
|
||||||
|
Type: String
|
||||||
|
Default: seeder
|
||||||
|
RegistryURL:
|
||||||
|
Type: String
|
||||||
|
Description: HTTPS URL agents poll (maps to AETHERFORGE_CLOUD_MAP_ENDPOINT)
|
||||||
|
Default: "{{SERVER_URL}}/api/v1/public/cloud-map/registry-endpoint.json"
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
SeederServiceNetwork:
|
||||||
|
Type: AWS::VpcLattice::ServiceNetwork
|
||||||
|
Properties:
|
||||||
|
Name: aetherforge-seeder-network
|
||||||
|
AuthType: NONE
|
||||||
|
|
||||||
|
SeederLatticeService:
|
||||||
|
Type: AWS::VpcLattice::Service
|
||||||
|
Properties:
|
||||||
|
Name: !Sub "${ServiceName}-lattice"
|
||||||
|
AuthType: NONE
|
||||||
|
|
||||||
|
SeederServiceNetworkAssociation:
|
||||||
|
Type: AWS::VpcLattice::ServiceNetworkServiceAssociation
|
||||||
|
Properties:
|
||||||
|
ServiceNetworkIdentifier: !GetAtt SeederServiceNetwork.Id
|
||||||
|
ServiceIdentifier: !GetAtt SeederLatticeService.Id
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
CloudMapSeederDNS:
|
||||||
|
Description: BGP route_via hint for spread plans
|
||||||
|
Value: !Sub "${ServiceName}.svc.${NamespaceName}"
|
||||||
|
AgentRegistryEndpoint:
|
||||||
|
Description: Set AETHERFORGE_CLOUD_MAP_ENDPOINT to this URL
|
||||||
|
Value: !Ref RegistryURL
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"namespace": "{{NAMESPACE_NAME}}",
|
"namespace": "{{NAMESPACE_NAME}}",
|
||||||
"service": "seeder",
|
"service": "seeder",
|
||||||
|
"updated_at": "2026-06-07T00:00:00Z",
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
"agent_id": "REPLACE_AGENT_ID",
|
"agent_id": "REPLACE_AGENT_ID",
|
||||||
|
|||||||
17
templates/cloud/cloud-map/service-registry.json
Normal file
17
templates/cloud/cloud-map/service-registry.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Name": "aetherforge-seeder",
|
||||||
|
"NamespaceId": "REPLACE_CLOUD_MAP_NAMESPACE_ID",
|
||||||
|
"Description": "AetherForge erasure seeder instances for VPC Lattice discovery",
|
||||||
|
"DnsConfig": {
|
||||||
|
"DnsRecords": [
|
||||||
|
{
|
||||||
|
"Type": "A",
|
||||||
|
"TTL": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RoutingPolicy": "MULTIVALUE"
|
||||||
|
},
|
||||||
|
"HealthCheckCustomConfig": {
|
||||||
|
"FailureThreshold": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user