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:
A 2026-03-10 17:56:43 -07:00 committed by GitHub
parent 95b5de040d
commit 6377b58bc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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}`);