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