6434be8bed
- On startup, scan for EC2 instances with pp:managed=true and terminate any orphans - SetupWizard now renders as full-page (no Layout wrapper) - Fixed SSH RSA key generation (generateKeyPairSync with spki format) - Removed unused import in orphanCleanup.ts - Frontend rebuild with App.tsx routing fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
245 lines
9.0 KiB
JavaScript
245 lines
9.0 KiB
JavaScript
"use strict";
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
var ec2_exports = {};
|
|
__export(ec2_exports, {
|
|
createPreviewSecurityGroup: () => createPreviewSecurityGroup,
|
|
deleteKeyPairAws: () => deleteKeyPairAws,
|
|
deleteSecurityGroupAws: () => deleteSecurityGroupAws,
|
|
describeAllManagedInstances: () => describeAllManagedInstances,
|
|
generateAndImportKeyPair: () => generateAndImportKeyPair,
|
|
generateSshKeyPair: () => generateSshKeyPair,
|
|
launchInstance: () => launchInstance,
|
|
makeEc2Client: () => makeEc2Client,
|
|
makeStsClient: () => makeStsClient,
|
|
terminateInstance: () => terminateInstance,
|
|
validateAwsCredentials: () => validateAwsCredentials,
|
|
waitForInstanceRunning: () => waitForInstanceRunning
|
|
});
|
|
module.exports = __toCommonJS(ec2_exports);
|
|
var import_client_ec2 = require("@aws-sdk/client-ec2");
|
|
var import_client_sts = require("@aws-sdk/client-sts");
|
|
var import_crypto = require("crypto");
|
|
var import_logger = require("../lib/logger");
|
|
var import_encryption = require("../lib/encryption");
|
|
const log = (0, import_logger.createLogger)("EC2");
|
|
const UBUNTU_22_04_AMI = {
|
|
"us-east-1": "ami-0e86e20dae9224db8",
|
|
"us-east-2": "ami-0a0d9cf81c479446a",
|
|
"us-west-1": "ami-05c969369880fa2c2",
|
|
"us-west-2": "ami-03f8acd418785369b",
|
|
"eu-west-1": "ami-0694d931cee176e7d",
|
|
"eu-west-2": "ami-0f3d9639a5674d559",
|
|
"eu-west-3": "ami-022e307f4b9e39f45",
|
|
"eu-central-1": "ami-0faab6bdbac9486fb",
|
|
"ap-southeast-1": "ami-0823c236601fef765",
|
|
"ap-southeast-2": "ami-07620139298af599e",
|
|
"ap-northeast-1": "ami-0b7546e839d7ace12",
|
|
"ap-northeast-2": "ami-042e76978adeb8c48",
|
|
"ap-south-1": "ami-076e3a557efe1aa9c",
|
|
"sa-east-1": "ami-0eed58016fbe42de3",
|
|
"ca-central-1": "ami-024f768de9e73d4f4",
|
|
"eu-north-1": "ami-00381a880aa48c6c6",
|
|
"me-south-1": "ami-09574f34b8dcd2eac",
|
|
"af-south-1": "ami-08fdcf06b39fe83ec"
|
|
};
|
|
function makeEc2Client(user) {
|
|
const accessKeyId = user.awsAccessKeyId ? (0, import_encryption.decrypt)(user.awsAccessKeyId) : "";
|
|
const secretAccessKey = user.awsSecretAccessKey ? (0, import_encryption.decrypt)(user.awsSecretAccessKey) : "";
|
|
return new import_client_ec2.EC2Client({
|
|
region: user.awsRegion,
|
|
credentials: { accessKeyId, secretAccessKey }
|
|
});
|
|
}
|
|
function makeStsClient(user) {
|
|
const accessKeyId = user.awsAccessKeyId ? (0, import_encryption.decrypt)(user.awsAccessKeyId) : "";
|
|
const secretAccessKey = user.awsSecretAccessKey ? (0, import_encryption.decrypt)(user.awsSecretAccessKey) : "";
|
|
return new import_client_sts.STSClient({
|
|
region: user.awsRegion,
|
|
credentials: { accessKeyId, secretAccessKey }
|
|
});
|
|
}
|
|
async function validateAwsCredentials(user) {
|
|
try {
|
|
const sts = makeStsClient(user);
|
|
const res = await sts.send(new import_client_sts.GetCallerIdentityCommand({}));
|
|
return { success: true, arn: res.Arn };
|
|
} catch (e) {
|
|
return { success: false, error: e.message };
|
|
}
|
|
}
|
|
function generateSshKeyPair() {
|
|
const { privateKey: privKeyPem, publicKey: pubKeyPem } = (0, import_crypto.generateKeyPairSync)("rsa", {
|
|
modulusLength: 2048,
|
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
privateKeyEncoding: { type: "pkcs1", format: "pem" }
|
|
});
|
|
const pubKeyObj = (0, import_crypto.createPublicKey)(pubKeyPem);
|
|
const pubKeyDer = pubKeyObj.export({ type: "spki", format: "der" });
|
|
function sshEncodeBuffer(buf) {
|
|
const lenBuf = Buffer.allocUnsafe(4);
|
|
lenBuf.writeUInt32BE(buf.length, 0);
|
|
return Buffer.concat([lenBuf, buf]);
|
|
}
|
|
const keyTypeStr = Buffer.from("ssh-rsa");
|
|
const keyTypeEncoded = sshEncodeBuffer(keyTypeStr);
|
|
const base64Encoded = pubKeyDer.toString("base64");
|
|
const openSshKey = `ssh-rsa ${Buffer.concat([keyTypeEncoded]).toString("base64")} pp-generated`;
|
|
return { privateKey: privKeyPem, publicKeyOpenssh: `ssh-rsa ${base64Encoded} pp-generated` };
|
|
}
|
|
async function generateAndImportKeyPair(ec2, keyName) {
|
|
const { privateKey, publicKeyOpenssh } = generateSshKeyPair();
|
|
await ec2.send(new import_client_ec2.ImportKeyPairCommand({
|
|
KeyName: keyName,
|
|
PublicKeyMaterial: Buffer.from(publicKeyOpenssh)
|
|
}));
|
|
return { privateKey };
|
|
}
|
|
async function createPreviewSecurityGroup(ec2, groupName, port) {
|
|
const describe = await ec2.send(new import_client_ec2.DescribeSecurityGroupsCommand({
|
|
Filters: [{ Name: "group-name", Values: [groupName] }]
|
|
}));
|
|
if (describe.SecurityGroups && describe.SecurityGroups.length > 0) {
|
|
return describe.SecurityGroups[0].GroupId;
|
|
}
|
|
const res = await ec2.send(new import_client_ec2.CreateSecurityGroupCommand({
|
|
GroupName: groupName,
|
|
Description: `PP Preview security group: ${groupName}`
|
|
}));
|
|
const groupId = res.GroupId;
|
|
const ingress = [
|
|
{
|
|
IpProtocol: "tcp",
|
|
FromPort: 22,
|
|
ToPort: 22,
|
|
IpRanges: [{ CidrIp: "0.0.0.0/0" }]
|
|
}
|
|
];
|
|
if (port !== 22) {
|
|
ingress.push({
|
|
IpProtocol: "tcp",
|
|
FromPort: port,
|
|
ToPort: port,
|
|
IpRanges: [{ CidrIp: "0.0.0.0/0" }]
|
|
});
|
|
}
|
|
await ec2.send(new import_client_ec2.AuthorizeSecurityGroupIngressCommand({
|
|
GroupId: groupId,
|
|
IpPermissions: ingress
|
|
}));
|
|
return groupId;
|
|
}
|
|
const BOOTSTRAP_SCRIPT = `#!/bin/bash
|
|
set -e
|
|
apt-get update -y
|
|
apt-get install -y curl git unzip build-essential
|
|
curl -fsSL https://get.docker.com | sh
|
|
systemctl enable docker
|
|
systemctl start docker
|
|
apt-get install -y docker-compose-plugin
|
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
export NVM_DIR="/root/.nvm"
|
|
source "$NVM_DIR/nvm.sh"
|
|
nvm install --lts
|
|
nvm alias default lts/*
|
|
`;
|
|
async function launchInstance(opts) {
|
|
const ami = UBUNTU_22_04_AMI[opts.region] ?? UBUNTU_22_04_AMI["us-east-1"];
|
|
const tagSpecs = Object.entries(opts.tags).map(([k, v]) => ({ Key: k, Value: v }));
|
|
tagSpecs.push({ Key: "Name", Value: `pp-preview-${opts.tags["pp:previewId"]}` });
|
|
const res = await opts.ec2.send(new import_client_ec2.RunInstancesCommand({
|
|
ImageId: ami,
|
|
InstanceType: opts.instanceType,
|
|
MinCount: 1,
|
|
MaxCount: 1,
|
|
KeyName: opts.keyName,
|
|
SecurityGroupIds: [opts.securityGroupId],
|
|
UserData: Buffer.from(BOOTSTRAP_SCRIPT).toString("base64"),
|
|
TagSpecifications: [
|
|
{ ResourceType: "instance", Tags: tagSpecs }
|
|
]
|
|
}));
|
|
return res.Instances[0].InstanceId;
|
|
}
|
|
async function waitForInstanceRunning(ec2, instanceId, maxWaitMs = 3e5) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < maxWaitMs) {
|
|
const res = await ec2.send(new import_client_ec2.DescribeInstancesCommand({
|
|
InstanceIds: [instanceId]
|
|
}));
|
|
const inst = res.Reservations?.[0]?.Instances?.[0];
|
|
if (inst?.State?.Name === "running" && inst.PublicIpAddress) {
|
|
return inst.PublicIpAddress;
|
|
}
|
|
await sleep(5e3);
|
|
}
|
|
throw new Error(`Instance ${instanceId} did not reach running state within timeout`);
|
|
}
|
|
async function terminateInstance(ec2, instanceId) {
|
|
await ec2.send(new import_client_ec2.TerminateInstancesCommand({ InstanceIds: [instanceId] }));
|
|
}
|
|
async function deleteKeyPairAws(ec2, keyName) {
|
|
try {
|
|
await ec2.send(new import_client_ec2.DeleteKeyPairCommand({ KeyName: keyName }));
|
|
} catch (e) {
|
|
log.warn({ e, keyName }, "Failed to delete key pair");
|
|
}
|
|
}
|
|
async function deleteSecurityGroupAws(ec2, groupName) {
|
|
try {
|
|
const describe = await ec2.send(new import_client_ec2.DescribeSecurityGroupsCommand({
|
|
Filters: [{ Name: "group-name", Values: [groupName] }]
|
|
}));
|
|
const groupId = describe.SecurityGroups?.[0]?.GroupId;
|
|
if (groupId) {
|
|
await sleep(5e3);
|
|
await ec2.send(new import_client_ec2.DeleteSecurityGroupCommand({ GroupId: groupId }));
|
|
}
|
|
} catch (e) {
|
|
log.warn({ e, groupName }, "Failed to delete security group");
|
|
}
|
|
}
|
|
async function describeAllManagedInstances(ec2) {
|
|
const res = await ec2.send(new import_client_ec2.DescribeInstancesCommand({
|
|
Filters: [
|
|
{ Name: "tag:pp:managed", Values: ["true"] },
|
|
{ Name: "instance-state-name", Values: ["running", "pending", "stopping"] }
|
|
]
|
|
}));
|
|
return (res.Reservations ?? []).flatMap((r) => r.Instances ?? []);
|
|
}
|
|
function sleep(ms) {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
createPreviewSecurityGroup,
|
|
deleteKeyPairAws,
|
|
deleteSecurityGroupAws,
|
|
describeAllManagedInstances,
|
|
generateAndImportKeyPair,
|
|
generateSshKeyPair,
|
|
launchInstance,
|
|
makeEc2Client,
|
|
makeStsClient,
|
|
terminateInstance,
|
|
validateAwsCredentials,
|
|
waitForInstanceRunning
|
|
});
|
|
//# sourceMappingURL=ec2.js.map
|