Files
AetherForge/server/internal/api/cloud_spread.go
AetherForge 5b5fc7c01c
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Emberwake Cloud Ecosystem deploy hub with AWS and generic spread templates.
Unified expandable panels with mermaid flows, ZIP export, connection tests, and Playwright smoke coverage.
2026-06-07 10:03:22 -07:00

199 lines
6.6 KiB
Go

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