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)
}
}