mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
refactor: extract Lightsail operation helpers to eliminate CLI/REST branching duplication (#2468)
The AWS module had CLI-vs-REST branching duplicated in ensureSshKey (2x), createInstance (4x), and waitForInstance (2x). Extracted 4 private helpers (lightsailGetKeyPair, lightsailImportKeyPair, lightsailCreateInstances, lightsailGetInstance) so each consumer is a single linear flow. A bug fix in one mode can no longer be missed in the other. Agent: complexity-hunter Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
95b5de040d
commit
6377b58bc1
1 changed files with 181 additions and 240 deletions
|
|
@ -691,6 +691,133 @@ export async function promptBundle(agentName?: string): Promise<void> {
|
|||
logInfo(`Using bundle: ${selected}`);
|
||||
}
|
||||
|
||||
// ─── Lightsail Operation Helpers ─────────────────────────────────────────────
|
||||
// These helpers abstract the CLI-vs-REST branching so each consumer is a single
|
||||
// linear flow instead of duplicating the branch in every function.
|
||||
|
||||
/** Check if a Lightsail key pair exists. Returns true if found, false otherwise. */
|
||||
async function lightsailGetKeyPair(keyPairName: string): Promise<boolean> {
|
||||
if (_state.lightsailMode === "cli") {
|
||||
return (
|
||||
awsCliSync([
|
||||
"lightsail",
|
||||
"get-key-pair",
|
||||
"--key-pair-name",
|
||||
keyPairName,
|
||||
]).exitCode === 0
|
||||
);
|
||||
}
|
||||
try {
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.GetKeyPair",
|
||||
JSON.stringify({
|
||||
keyPairName,
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Import a public key to Lightsail as a key pair. */
|
||||
async function lightsailImportKeyPair(keyPairName: string, publicKeyBase64: string): Promise<void> {
|
||||
if (_state.lightsailMode === "cli") {
|
||||
await awsCli([
|
||||
"lightsail",
|
||||
"import-key-pair",
|
||||
"--key-pair-name",
|
||||
keyPairName,
|
||||
"--public-key-base64",
|
||||
publicKeyBase64,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.ImportKeyPair",
|
||||
JSON.stringify({
|
||||
keyPairName,
|
||||
publicKeyBase64,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Create Lightsail instances. */
|
||||
async function lightsailCreateInstances(params: {
|
||||
name: string;
|
||||
az: string;
|
||||
blueprint: string;
|
||||
bundle: string;
|
||||
keyPairName: string;
|
||||
userData: string;
|
||||
}): Promise<void> {
|
||||
if (_state.lightsailMode === "cli") {
|
||||
await awsCli([
|
||||
"lightsail",
|
||||
"create-instances",
|
||||
"--instance-names",
|
||||
params.name,
|
||||
"--availability-zone",
|
||||
params.az,
|
||||
"--blueprint-id",
|
||||
params.blueprint,
|
||||
"--bundle-id",
|
||||
params.bundle,
|
||||
"--key-pair-name",
|
||||
params.keyPairName,
|
||||
"--user-data",
|
||||
params.userData,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.CreateInstances",
|
||||
JSON.stringify({
|
||||
instanceNames: [
|
||||
params.name,
|
||||
],
|
||||
availabilityZone: params.az,
|
||||
blueprintId: params.blueprint,
|
||||
bundleId: params.bundle,
|
||||
keyPairName: params.keyPairName,
|
||||
userData: params.userData,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Get Lightsail instance state and public IP. */
|
||||
async function lightsailGetInstance(instanceName: string): Promise<{
|
||||
state: string;
|
||||
ip: string;
|
||||
}> {
|
||||
if (_state.lightsailMode === "cli") {
|
||||
const resp = await awsCli([
|
||||
"lightsail",
|
||||
"get-instance",
|
||||
"--instance-name",
|
||||
instanceName,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
const data = parseJsonWith(resp, InstanceStateSchema);
|
||||
return {
|
||||
state: data?.instance?.state?.name || "",
|
||||
ip: data?.instance?.publicIpAddress || "",
|
||||
};
|
||||
}
|
||||
const resp = await lightsailRest(
|
||||
"Lightsail_20161128.GetInstance",
|
||||
JSON.stringify({
|
||||
instanceName,
|
||||
}),
|
||||
);
|
||||
const data = parseJsonWith(resp, InstanceStateSchema);
|
||||
return {
|
||||
state: data?.instance?.state?.name || "",
|
||||
ip: data?.instance?.publicIpAddress || "",
|
||||
};
|
||||
}
|
||||
|
||||
// ─── SSH Key Management ─────────────────────────────────────────────────────
|
||||
|
||||
export async function ensureSshKey(): Promise<void> {
|
||||
|
|
@ -706,93 +833,27 @@ export async function ensureSshKey(): Promise<void> {
|
|||
const keyName = "spawn-key";
|
||||
const pubKey = readFileSync(pubPath, "utf-8").trim();
|
||||
|
||||
if (_state.lightsailMode === "cli") {
|
||||
// Check if already registered
|
||||
const check = awsCliSync([
|
||||
"lightsail",
|
||||
"get-key-pair",
|
||||
"--key-pair-name",
|
||||
keyName,
|
||||
]);
|
||||
if (check.exitCode === 0) {
|
||||
logInfo("SSH key already registered with Lightsail");
|
||||
return;
|
||||
}
|
||||
|
||||
logStep("Importing SSH key to Lightsail...");
|
||||
try {
|
||||
await awsCli([
|
||||
"lightsail",
|
||||
"import-key-pair",
|
||||
"--key-pair-name",
|
||||
keyName,
|
||||
"--public-key-base64",
|
||||
pubKey,
|
||||
]);
|
||||
} catch {
|
||||
// Race condition: another process may have imported it
|
||||
const recheck = awsCliSync([
|
||||
"lightsail",
|
||||
"get-key-pair",
|
||||
"--key-pair-name",
|
||||
keyName,
|
||||
]);
|
||||
if (recheck.exitCode === 0) {
|
||||
logInfo("SSH key already registered with Lightsail");
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
"Failed to import SSH key to Lightsail. " +
|
||||
"On new AWS accounts, Lightsail may not be enabled. " +
|
||||
"Visit https://lightsail.aws.amazon.com/ to activate it, then try again.",
|
||||
);
|
||||
}
|
||||
logInfo("SSH key imported to Lightsail");
|
||||
} else {
|
||||
// REST path
|
||||
try {
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.GetKeyPair",
|
||||
JSON.stringify({
|
||||
keyPairName: keyName,
|
||||
}),
|
||||
);
|
||||
logInfo("SSH key already registered with Lightsail");
|
||||
return;
|
||||
} catch {
|
||||
// Key doesn't exist, import it
|
||||
}
|
||||
|
||||
logStep("Importing SSH key to Lightsail via REST API...");
|
||||
try {
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.ImportKeyPair",
|
||||
JSON.stringify({
|
||||
keyPairName: keyName,
|
||||
publicKeyBase64: pubKey,
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Race condition check
|
||||
try {
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.GetKeyPair",
|
||||
JSON.stringify({
|
||||
keyPairName: keyName,
|
||||
}),
|
||||
);
|
||||
logInfo("SSH key already registered with Lightsail");
|
||||
return;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to import SSH key to Lightsail. " +
|
||||
"On new AWS accounts, Lightsail may not be enabled. " +
|
||||
"Visit https://lightsail.aws.amazon.com/ to activate it, then try again.",
|
||||
);
|
||||
}
|
||||
}
|
||||
logInfo("SSH key imported to Lightsail");
|
||||
if (await lightsailGetKeyPair(keyName)) {
|
||||
logInfo("SSH key already registered with Lightsail");
|
||||
return;
|
||||
}
|
||||
|
||||
logStep("Importing SSH key to Lightsail...");
|
||||
try {
|
||||
await lightsailImportKeyPair(keyName, pubKey);
|
||||
} catch {
|
||||
// Race condition: another process may have imported it
|
||||
if (await lightsailGetKeyPair(keyName)) {
|
||||
logInfo("SSH key already registered with Lightsail");
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
"Failed to import SSH key to Lightsail. " +
|
||||
"On new AWS accounts, Lightsail may not be enabled. " +
|
||||
"Visit https://lightsail.aws.amazon.com/ to activate it, then try again.",
|
||||
);
|
||||
}
|
||||
logInfo("SSH key imported to Lightsail");
|
||||
}
|
||||
|
||||
// ─── Cloud-init User Data ───────────────────────────────────────────────────
|
||||
|
|
@ -851,115 +912,40 @@ export async function createInstance(name: string, tier?: CloudInitTier): Promis
|
|||
logStep(`Creating Lightsail instance '${name}' (bundle: ${bundle}, AZ: ${az})...`);
|
||||
|
||||
const userdata = getCloudInitUserdata(tier);
|
||||
const createParams = {
|
||||
name,
|
||||
az,
|
||||
blueprint,
|
||||
bundle,
|
||||
keyPairName: "spawn-key",
|
||||
userData: userdata,
|
||||
};
|
||||
|
||||
if (_state.lightsailMode === "cli") {
|
||||
try {
|
||||
await awsCli([
|
||||
"lightsail",
|
||||
"create-instances",
|
||||
"--instance-names",
|
||||
name,
|
||||
"--availability-zone",
|
||||
az,
|
||||
"--blueprint-id",
|
||||
blueprint,
|
||||
"--bundle-id",
|
||||
bundle,
|
||||
"--key-pair-name",
|
||||
"spawn-key",
|
||||
"--user-data",
|
||||
userdata,
|
||||
try {
|
||||
await lightsailCreateInstances(createParams);
|
||||
} catch (err) {
|
||||
const errMsg = getErrorMessage(err);
|
||||
logError(`Failed to create Lightsail instance: ${errMsg}`);
|
||||
|
||||
if (isBillingError("aws", errMsg)) {
|
||||
const shouldRetry = await handleBillingError("aws");
|
||||
if (shouldRetry) {
|
||||
logStep("Retrying instance creation...");
|
||||
await lightsailCreateInstances(createParams);
|
||||
_state.instanceName = name;
|
||||
logInfo(`Instance creation initiated: ${name}`);
|
||||
return await waitForInstance();
|
||||
}
|
||||
} else {
|
||||
showNonBillingError("aws", [
|
||||
"Lightsail not enabled: visit https://lightsail.aws.amazon.com/ls/webapp/home to activate",
|
||||
"Instance limit reached for your account",
|
||||
"Bundle unavailable in region",
|
||||
"AWS credentials lack Lightsail permissions",
|
||||
`Instance name '${name}' already in use`,
|
||||
]);
|
||||
} catch (err) {
|
||||
const errMsg = getErrorMessage(err);
|
||||
logError(`Failed to create Lightsail instance: ${errMsg}`);
|
||||
|
||||
if (isBillingError("aws", errMsg)) {
|
||||
const shouldRetry = await handleBillingError("aws");
|
||||
if (shouldRetry) {
|
||||
logStep("Retrying instance creation...");
|
||||
await awsCli([
|
||||
"lightsail",
|
||||
"create-instances",
|
||||
"--instance-names",
|
||||
name,
|
||||
"--availability-zone",
|
||||
az,
|
||||
"--blueprint-id",
|
||||
blueprint,
|
||||
"--bundle-id",
|
||||
bundle,
|
||||
"--key-pair-name",
|
||||
"spawn-key",
|
||||
"--user-data",
|
||||
userdata,
|
||||
]);
|
||||
_state.instanceName = name;
|
||||
logInfo(`Instance creation initiated: ${name}`);
|
||||
return await waitForInstance();
|
||||
}
|
||||
} else {
|
||||
showNonBillingError("aws", [
|
||||
"Lightsail not enabled: visit https://lightsail.aws.amazon.com/ls/webapp/home to activate",
|
||||
"Instance limit reached for your account",
|
||||
"Bundle unavailable in region",
|
||||
"AWS credentials lack Lightsail permissions",
|
||||
`Instance name '${name}' already in use`,
|
||||
]);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.CreateInstances",
|
||||
JSON.stringify({
|
||||
instanceNames: [
|
||||
name,
|
||||
],
|
||||
availabilityZone: az,
|
||||
blueprintId: blueprint,
|
||||
bundleId: bundle,
|
||||
keyPairName: "spawn-key",
|
||||
userData: userdata,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
const errMsg = getErrorMessage(err);
|
||||
logError(`Failed to create Lightsail instance: ${errMsg}`);
|
||||
|
||||
if (isBillingError("aws", errMsg)) {
|
||||
const shouldRetry = await handleBillingError("aws");
|
||||
if (shouldRetry) {
|
||||
logStep("Retrying instance creation...");
|
||||
await lightsailRest(
|
||||
"Lightsail_20161128.CreateInstances",
|
||||
JSON.stringify({
|
||||
instanceNames: [
|
||||
name,
|
||||
],
|
||||
availabilityZone: az,
|
||||
blueprintId: blueprint,
|
||||
bundleId: bundle,
|
||||
keyPairName: "spawn-key",
|
||||
userData: userdata,
|
||||
}),
|
||||
);
|
||||
_state.instanceName = name;
|
||||
logInfo(`Instance creation initiated: ${name}`);
|
||||
return await waitForInstance();
|
||||
}
|
||||
} else {
|
||||
showNonBillingError("aws", [
|
||||
"Lightsail not enabled: visit https://lightsail.aws.amazon.com/ls/webapp/home to activate",
|
||||
"Instance limit reached for your account",
|
||||
"Bundle unavailable in region",
|
||||
"Credentials lack lightsail:CreateInstances permission",
|
||||
`Instance name '${name}' already in use`,
|
||||
]);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
_state.instanceName = name;
|
||||
|
|
@ -980,59 +966,14 @@ async function waitForInstance(maxAttempts = 60): Promise<VMConnection> {
|
|||
let ip = "";
|
||||
|
||||
try {
|
||||
if (_state.lightsailMode === "cli") {
|
||||
const resp = await awsCli([
|
||||
"lightsail",
|
||||
"get-instance",
|
||||
"--instance-name",
|
||||
_state.instanceName,
|
||||
"--query",
|
||||
"instance.state.name",
|
||||
"--output",
|
||||
"text",
|
||||
]);
|
||||
state = resp.trim();
|
||||
} else {
|
||||
const resp = await lightsailRest(
|
||||
"Lightsail_20161128.GetInstance",
|
||||
JSON.stringify({
|
||||
instanceName: _state.instanceName,
|
||||
}),
|
||||
);
|
||||
const data = parseJsonWith(resp, InstanceStateSchema);
|
||||
state = data?.instance?.state?.name || "";
|
||||
}
|
||||
const info = await lightsailGetInstance(_state.instanceName);
|
||||
state = info.state;
|
||||
ip = info.ip;
|
||||
} catch {
|
||||
state = "";
|
||||
}
|
||||
|
||||
if (state === "running") {
|
||||
try {
|
||||
if (_state.lightsailMode === "cli") {
|
||||
ip = await awsCli([
|
||||
"lightsail",
|
||||
"get-instance",
|
||||
"--instance-name",
|
||||
_state.instanceName,
|
||||
"--query",
|
||||
"instance.publicIpAddress",
|
||||
"--output",
|
||||
"text",
|
||||
]);
|
||||
} else {
|
||||
const resp = await lightsailRest(
|
||||
"Lightsail_20161128.GetInstance",
|
||||
JSON.stringify({
|
||||
instanceName: _state.instanceName,
|
||||
}),
|
||||
);
|
||||
const data = parseJsonWith(resp, InstanceStateSchema);
|
||||
ip = data?.instance?.publicIpAddress || "";
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
_state.instanceIp = ip.trim();
|
||||
logStepDone();
|
||||
logInfo(`Instance running: IP=${_state.instanceIp}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue