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.
199 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|