chore: auto-commit uncommitted changes

This commit is contained in:
James 2026-03-26 12:01:24 -04:00
parent 0922dde30a
commit 14b6079a61
7 changed files with 223 additions and 0 deletions

Binary file not shown.

Binary file not shown.

View File

@ -14,6 +14,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect

View File

@ -20,6 +20,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.3 h1:bBoWhx8lsFLTXintRX64ZBXcmFZbGqUmaPUrjXECqIc=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.3/go.mod h1:rcRkKbUJ2437WuXdq9fbj+MjTudYWzY9Ct8kiBbN8a8=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=

View File

@ -19,6 +19,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/ssm"
_ "modernc.org/sqlite"
)
@ -120,6 +121,11 @@ func main() {
exitWith(cmdExec(cfg, strings.Join(remaining, " ")))
case "firewall":
exitWith(cmdFirewall(cfg))
case "provision":
if len(remaining) == 0 {
fatal("usage: pop-sync provision <city> [city2 ...] (e.g. pop-sync provision Tokyo Calgary)")
}
exitWith(cmdProvision(cfg, remaining))
case "maintenance":
if len(remaining) == 0 {
fatal("usage: pop-sync maintenance <on|off> [reason]")
@ -728,6 +734,220 @@ func ensureFirewall(cfg Config, pop POP) NodeResult {
}
func strPtr(s string) *string { return &s }
func int32Ptr(i int32) *int32 { return &i }
// --- Subcommand: provision ---
// Spins up t4g.nano EC2 instances for planned POPs.
func cmdProvision(cfg Config, cities []string) []NodeResult {
if cfg.AWSKeyID == "" || cfg.AWSSecretKey == "" {
fatal("AWS credentials required: set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or -aws-key/-aws-secret")
}
// Load all POPs from DB
allPOPs, err := readPOPs(cfg.DBPath)
if err != nil {
fatal("reading DB: %v", err)
}
// Match cities to planned POPs
var targets []POP
for _, city := range cities {
found := false
for _, p := range allPOPs {
if strings.EqualFold(p.City, city) && p.Status == "planned" {
targets = append(targets, p)
found = true
break
}
}
if !found {
log(cfg, "WARNING: %q not found or not planned, skipping", city)
}
}
if len(targets) == 0 {
fatal("no matching planned POPs found")
}
log(cfg, "Provisioning %d nodes...\n", len(targets))
var results []NodeResult
for _, p := range targets {
r := provisionNode(cfg, p)
results = append(results, r)
}
outputResults(cfg, results)
return results
}
func provisionNode(cfg Config, pop POP) NodeResult {
region := pop.RegionName
r := NodeResult{Node: pop.City, Action: "provision"}
ctx := context.Background()
awsCfg, err := awsconfig.LoadDefaultConfig(ctx,
awsconfig.WithRegion(region),
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSKeyID, cfg.AWSSecretKey, "")),
)
if err != nil {
r.Error = fmt.Sprintf("aws config: %v", err)
return r
}
ec2Client := ec2.NewFromConfig(awsCfg)
ssmClient := ssm.NewFromConfig(awsCfg)
// 1. Look up latest AL2023 ARM64 AMI
log(cfg, " [%s] looking up AMI in %s...", pop.City, region)
amiParam, err := ssmClient.GetParameter(ctx, &ssm.GetParameterInput{
Name: strPtr("/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"),
})
if err != nil {
r.Error = fmt.Sprintf("AMI lookup: %v", err)
return r
}
amiID := *amiParam.Parameter.Value
log(cfg, " [%s] AMI: %s", pop.City, amiID)
// 2. Find or create security group
sgID, err := ensureSecurityGroup(ctx, ec2Client, region, pop.City, cfg)
if err != nil {
r.Error = fmt.Sprintf("security group: %v", err)
return r
}
log(cfg, " [%s] SG: %s", pop.City, sgID)
// 3. Determine subdomain for the node
sub := pop.Subdomain()
if sub == "" {
// Generate from country code + ordinal
sub = strings.ToLower(pop.Country) + "1"
}
if cfg.DryRun {
r.OK = true
r.Message = fmt.Sprintf("would launch t4g.nano in %s (AMI: %s, SG: %s)", region, amiID, sgID)
log(cfg, " [%s] DRY RUN: %s", pop.City, r.Message)
return r
}
// 4. Launch instance
log(cfg, " [%s] launching t4g.nano...", pop.City)
runOut, err := ec2Client.RunInstances(ctx, &ec2.RunInstancesInput{
ImageId: &amiID,
InstanceType: ec2types.InstanceTypeT4gNano,
MinCount: int32Ptr(1),
MaxCount: int32Ptr(1),
SecurityGroupIds: []string{sgID},
IamInstanceProfile: &ec2types.IamInstanceProfileSpecification{
Name: strPtr("vault1984-ssm-profile"),
},
TagSpecifications: []ec2types.TagSpecification{{
ResourceType: ec2types.ResourceTypeInstance,
Tags: []ec2types.Tag{
{Key: strPtr("Name"), Value: strPtr("clavitor-" + sub)},
{Key: strPtr("clavitor-pop"), Value: strPtr(sub)},
},
}},
})
if err != nil {
r.Error = fmt.Sprintf("launch: %v", err)
return r
}
instanceID := *runOut.Instances[0].InstanceId
log(cfg, " [%s] launched: %s, waiting for public IP...", pop.City, instanceID)
// 5. Wait for running + public IP
waiter := ec2.NewInstanceRunningWaiter(ec2Client)
err = waiter.Wait(ctx, &ec2.DescribeInstancesInput{
InstanceIds: []string{instanceID},
}, 3*time.Minute)
if err != nil {
r.Error = fmt.Sprintf("wait running: %v", err)
return r
}
// Get public IP
descOut, err := ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
InstanceIds: []string{instanceID},
})
if err != nil || len(descOut.Reservations) == 0 || len(descOut.Reservations[0].Instances) == 0 {
r.Error = fmt.Sprintf("describe: %v", err)
return r
}
inst := descOut.Reservations[0].Instances[0]
publicIP := ""
if inst.PublicIpAddress != nil {
publicIP = *inst.PublicIpAddress
}
if publicIP == "" {
r.Error = "no public IP assigned"
return r
}
log(cfg, " [%s] IP: %s", pop.City, publicIP)
// 6. Update DB
dns := sub + "." + cfg.Zone
db, err := sql.Open("sqlite", cfg.DBPath)
if err != nil {
r.Error = fmt.Sprintf("open db: %v", err)
return r
}
defer db.Close()
_, err = db.Exec(`UPDATE pops SET instance_id=?, ip=?, dns=?, status='live' WHERE pop_id=?`,
instanceID, publicIP, dns, pop.PopID)
if err != nil {
r.Error = fmt.Sprintf("update db: %v", err)
return r
}
r.OK = true
r.Message = fmt.Sprintf("launched %s (%s) → %s → %s", instanceID, region, publicIP, dns)
log(cfg, " [%s] DONE: %s", pop.City, r.Message)
return r
}
func ensureSecurityGroup(ctx context.Context, client *ec2.Client, region, city string, cfg Config) (string, error) {
// Check if vault1984-pop exists in this region
descOut, err := client.DescribeSecurityGroups(ctx, &ec2.DescribeSecurityGroupsInput{
Filters: []ec2types.Filter{{
Name: strPtr("group-name"),
Values: []string{"vault1984-pop"},
}},
})
if err == nil && len(descOut.SecurityGroups) > 0 {
return *descOut.SecurityGroups[0].GroupId, nil
}
// Create it
log(cfg, " [%s] creating security group vault1984-pop in %s...", city, region)
createOut, err := client.CreateSecurityGroup(ctx, &ec2.CreateSecurityGroupInput{
GroupName: strPtr("vault1984-pop"),
Description: strPtr("Clavitor POP - port 1984 only"),
})
if err != nil {
return "", fmt.Errorf("create sg: %w", err)
}
sgID := *createOut.GroupId
// Add port 1984
proto := "tcp"
port := int32(1984)
_, err = client.AuthorizeSecurityGroupIngress(ctx, &ec2.AuthorizeSecurityGroupIngressInput{
GroupId: &sgID,
IpPermissions: []ec2types.IpPermission{{
IpProtocol: &proto,
FromPort: &port,
ToPort: &port,
IpRanges: []ec2types.IpRange{{CidrIp: strPtr("0.0.0.0/0")}},
}},
})
if err != nil {
return sgID, fmt.Errorf("add rule: %w", err)
}
return sgID, nil
}
// --- Node execution: Tailscale SSH with SSM fallback ---

Binary file not shown.