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
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:
198
server/internal/api/cloud_spread.go
Normal file
198
server/internal/api/cloud_spread.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user