Files
AetherForge/server/internal/builder/launch_template.go
AetherForge 7191bda6fd Add EC2 Launch Template strain genesis for AWS horizontal scale.
Ship POST /api/v1/forge/launch-template with cloud-init user-data embedding genesis snapshot hash and strain card ID; first template-tagged auth pins SpreadGeneration=0 and ParentAgentID=template.
2026-06-07 09:59:29 -07:00

170 lines
6.0 KiB
Go

package builder
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
)
const (
JoinLaneLaunchTemplate = "launch_template"
LaunchTemplateParentAgentID = "template"
defaultLaunchTemplateAMI = "ami-0c55b159cbfafe1f0"
defaultLaunchTemplateInst = "t3.small"
launchTemplateName = "aetherforge-strain-genesis"
)
type LaunchTemplateRequest struct {
ServerURL string `json:"server_url"`
BuildID string `json:"build_id,omitempty"`
StrainCardID string `json:"strain_card_id,omitempty"`
Campaign string `json:"campaign,omitempty"`
AMIID string `json:"ami_id,omitempty"`
InstanceType string `json:"instance_type,omitempty"`
Region string `json:"region,omitempty"`
}
type LaunchTemplateResponse struct {
Success bool `json:"success"`
GenesisSnapshotHash string `json:"genesis_snapshot_hash"`
StrainCardID string `json:"strain_card_id"`
LaunchTemplateJSON string `json:"launch_template_json"`
UserDataSh string `json:"user_data_sh"`
ASGExampleJSON string `json:"asg_example_json"`
Error string `json:"error,omitempty"`
}
type genesisSnapshotManifest struct {
Version int `json:"version"`
ServerURL string `json:"server_url"`
BuildID string `json:"build_id,omitempty"`
StrainCardID string `json:"strain_card_id,omitempty"`
JoinLane string `json:"join_lane"`
Campaign string `json:"campaign,omitempty"`
}
func GenesisSnapshotHash(m genesisSnapshotManifest) string {
m.Version = 1
m.JoinLane = JoinLaneLaunchTemplate
raw, _ := json.Marshal(m)
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
func BuildLaunchTemplateArtifacts(req LaunchTemplateRequest) (LaunchTemplateResponse, error) {
serverURL := strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
if serverURL == "" {
return LaunchTemplateResponse{Success: false, Error: "server_url required"}, fmt.Errorf("server_url required")
}
buildID := strings.TrimSpace(req.BuildID)
strainCardID := strings.TrimSpace(req.StrainCardID)
campaign := strings.TrimSpace(req.Campaign)
amiID := strings.TrimSpace(req.AMIID)
if amiID == "" {
amiID = defaultLaunchTemplateAMI
}
instanceType := strings.TrimSpace(req.InstanceType)
if instanceType == "" {
instanceType = defaultLaunchTemplateInst
}
region := strings.TrimSpace(req.Region)
if region == "" {
region = "us-east-1"
}
genesisHash := GenesisSnapshotHash(genesisSnapshotManifest{
ServerURL: serverURL, BuildID: buildID, StrainCardID: strainCardID, Campaign: campaign,
})
querySuffix := buildLaunchTemplateQuerySuffix(buildID, campaign)
userData := buildLaunchTemplateUserData(serverURL, genesisHash, strainCardID, campaign, querySuffix)
userDataB64 := base64.StdEncoding.EncodeToString([]byte(userData))
ltPayload := map[string]interface{}{
"LaunchTemplateName": launchTemplateName,
"VersionDescription": fmt.Sprintf("AetherForge strain genesis snapshot %s", genesisHash[:12]),
"LaunchTemplateData": map[string]interface{}{
"ImageId": amiID, "InstanceType": instanceType, "UserData": userDataB64,
"TagSpecifications": []map[string]interface{}{{
"ResourceType": "instance",
"Tags": []map[string]string{
{"Key": "Name", "Value": "aetherforge-genesis"},
{"Key": "aetherforge:genesis-snapshot", "Value": genesisHash},
{"Key": "aetherforge:strain-card", "Value": strainCardID},
{"Key": "aetherforge:join-lane", "Value": JoinLaneLaunchTemplate},
},
}},
},
}
ltJSON, err := json.MarshalIndent(ltPayload, "", " ")
if err != nil {
return LaunchTemplateResponse{Success: false, Error: err.Error()}, err
}
asgPayload := map[string]interface{}{
"AutoScalingGroupName": "aetherforge-strain-genesis-asg",
"LaunchTemplate": map[string]interface{}{"LaunchTemplateName": launchTemplateName, "Version": "$Latest"},
"MinSize": 1, "MaxSize": 10, "DesiredCapacity": 2,
"VPCZoneIdentifier": []string{"subnet-xxxxxxxx"},
"Tags": []map[string]interface{}{
{"Key": "aetherforge:genesis-snapshot", "Value": genesisHash, "PropagateAtLaunch": true},
{"Key": "aetherforge:strain-card", "Value": strainCardID, "PropagateAtLaunch": true},
{"Key": "aetherforge:region", "Value": region, "PropagateAtLaunch": false},
},
}
asgJSON, err := json.MarshalIndent(asgPayload, "", " ")
if err != nil {
return LaunchTemplateResponse{Success: false, Error: err.Error()}, err
}
return LaunchTemplateResponse{
Success: true, GenesisSnapshotHash: genesisHash, StrainCardID: strainCardID,
LaunchTemplateJSON: string(ltJSON), UserDataSh: userData, ASGExampleJSON: string(asgJSON),
}, nil
}
func buildLaunchTemplateQuerySuffix(buildID, campaign string) string {
var parts []string
if buildID != "" {
parts = append(parts, "pin="+buildID)
}
if campaign != "" {
parts = append(parts, "c="+campaign)
}
sort.Strings(parts)
if len(parts) == 0 {
return ""
}
return "?" + strings.Join(parts, "&")
}
func buildLaunchTemplateUserData(serverURL, genesisHash, strainCardID, campaign, querySuffix string) string {
var b strings.Builder
b.WriteString("#!/bin/bash\n# AetherForge EC2 launch-template strain genesis\n")
b.WriteString("set -euo pipefail\nexport AETHER_PARENT_AGENT_ID=")
b.WriteString(jsonStringLiteral(LaunchTemplateParentAgentID))
b.WriteString("\nexport AETHER_SPREAD_GENERATION=0\nexport AETHER_GENESIS_SNAPSHOT_HASH=")
b.WriteString(jsonStringLiteral(genesisHash))
b.WriteString("\n")
if strainCardID != "" {
b.WriteString("export AETHER_STRAIN_CARD_ID=")
b.WriteString(jsonStringLiteral(strainCardID))
b.WriteString("\n")
}
if campaign != "" {
b.WriteString("export AETHER_CAMPAIGN=")
b.WriteString(jsonStringLiteral(campaign))
b.WriteString("\nexport AETHER_UTM=")
b.WriteString(jsonStringLiteral(campaign))
b.WriteString("\n")
}
b.WriteString("\ncurl -fsSL ")
b.WriteString(jsonStringLiteral(serverURL + "/install.sh" + querySuffix))
b.WriteString(" | bash\n")
return b.String()
}
func jsonStringLiteral(s string) string {
raw, _ := json.Marshal(s)
return string(raw)
}