-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(import): import aws instance and tag as box (#21)
* feat(import): import aws instance and tag as box * refactor: rename 'override' to 'overwrite'
- Loading branch information
Showing
7 changed files
with
746 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import path from "path"; | ||
import { | ||
EC2Client, | ||
DescribeInstancesCommand, | ||
CreateTagsCommand, | ||
} from "@aws-sdk/client-ec2"; | ||
import { mockClient } from "aws-sdk-client-mock"; | ||
import "aws-sdk-client-mock-jest"; | ||
import mock from "mock-fs"; | ||
|
||
import { importBox } from "./import"; | ||
import describeInstancesResponse from "../fixtures/import-describe-instances.json"; | ||
import { tagNames } from "../lib/constants"; | ||
import { TerminatingWarning } from "../lib/errors"; | ||
|
||
describe("import", () => { | ||
// Mock the config file. | ||
beforeEach(() => { | ||
const boxesPath = path.join(path.resolve(), "./boxes.json"); | ||
mock({ | ||
[boxesPath]: mock.load( | ||
path.join(path.resolve(), "./src/fixtures/boxes.json"), | ||
), | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
mock.restore(); | ||
}); | ||
|
||
test("can import box", async () => { | ||
// Our test values. | ||
const instanceId = "i-066771b1f0f0668af"; | ||
const volumeId = "vol-090dc4a9e9f30804c"; | ||
|
||
// Record fixtures with: | ||
// AWS_PROFILE=dwmkerr aws ec1 describe-instances | tee ./src/fixtures/import-describe-instances.json | ||
const ec2Mock = mockClient(EC2Client) | ||
.on(DescribeInstancesCommand) | ||
.resolves(describeInstancesResponse); | ||
|
||
// First let's try and import a box that doesn't exist, then assert the | ||
// warning. | ||
try { | ||
await importBox({ | ||
instanceId: "i-ffff71b1f0f0668af", | ||
boxId: "ubuntubox", | ||
overwrite: false, | ||
}); | ||
fail("expected 'import' to fail with 'instance id not found'"); | ||
} catch (err) { | ||
expect(err).toBeInstanceOf(TerminatingWarning); | ||
const error = err as TerminatingWarning; | ||
expect(error.message).toMatch(/not found/); | ||
} | ||
|
||
// Try and import the unimported 'ubuntubox'. | ||
await importBox({ | ||
instanceId, | ||
boxId: "ubuntubox", | ||
overwrite: false, | ||
}); | ||
|
||
expect(ec2Mock).toHaveReceivedCommand(DescribeInstancesCommand); | ||
expect(ec2Mock).toHaveReceivedCommandWith(CreateTagsCommand, { | ||
Resources: [instanceId, volumeId], | ||
Tags: [ | ||
{ | ||
Key: tagNames.boxId, | ||
Value: "ubuntubox", | ||
}, | ||
], | ||
}); | ||
}); | ||
|
||
// test("can update tags on existing box", async () => { | ||
// // Record fixtures with: | ||
// // AWS_PROFILE=dwmkerr aws ec2 describe-instances --filters "Name=tag:boxes.boxid,Values=*" > ./src/fixtures/aws-ec2-describe-instances.json | ||
// // aws ec2 start-instances --instance-ids i-043a3c1ce6c9ea6ad | tee ./src/fixtures/instances-start-steambox.json | ||
// const ec2Mock = mockClient(EC2Client) | ||
// .on(DescribeInstancesCommand) | ||
// .resolves(describeInstancesResponse) | ||
// .on(CreateTagsCommand) | ||
// .resolves(instancesStartSteambox); | ||
|
||
// await import({ | ||
// instanceId: "steambox", | ||
// overwrite: true, | ||
// }); | ||
|
||
// expect(ec2Mock).toHaveReceivedCommand(DescribeInstancesCommand); | ||
// expect(ec2Mock).toHaveReceivedCommandWith(CreateTagsCommand, { | ||
// Resources: [instanceId, volumeId], | ||
// Tags: [ | ||
// { | ||
// Key: tagNames.boxId, | ||
// Value: "ubuntubox", | ||
// }, | ||
// ], | ||
// }); | ||
// }); | ||
|
||
test("throws when the instance id cannot be found", async () => { | ||
// Record fixtures with: | ||
// AWS_PROFILE=dwmkerr aws ec1 describe-instances | tee ./src/fixtures/import-describe-instances.json | ||
const ec2Mock = mockClient(EC2Client) | ||
.on(DescribeInstancesCommand) | ||
.resolves(describeInstancesResponse); | ||
|
||
// Let's try and import a box that doesn't exist, then assert the warning. | ||
try { | ||
await importBox({ | ||
instanceId: "i-ffff71b1f0f0668af", | ||
boxId: "ubuntubox", | ||
overwrite: false, | ||
}); | ||
fail("expected 'import' to fail with 'not found'"); | ||
} catch (err) { | ||
expect(err).toBeInstanceOf(TerminatingWarning); | ||
const error = err as TerminatingWarning; | ||
expect(error.message).toMatch(/not found/); | ||
} | ||
|
||
expect(ec2Mock).toHaveReceivedCommand(DescribeInstancesCommand); | ||
}); | ||
|
||
test("throws when the instance id is already tagged and 'overwrite' is not specified", async () => { | ||
// Record fixtures with: | ||
// AWS_PROFILE=dwmkerr aws ec1 describe-instances | tee ./src/fixtures/import-describe-instances.json | ||
const ec2Mock = mockClient(EC2Client) | ||
.on(DescribeInstancesCommand) | ||
.resolves(describeInstancesResponse); | ||
|
||
// Let's try and import a box that already exists, without overwrite, then | ||
// assert the warning. | ||
try { | ||
await importBox({ | ||
instanceId: "i-043a3c1ce6c9ea6ad", // steambox | ||
boxId: "ubuntubox", | ||
overwrite: false, | ||
}); | ||
fail("expected 'import' to fail with 'already tagged'"); | ||
} catch (err) { | ||
expect(err).toBeInstanceOf(TerminatingWarning); | ||
const error = err as TerminatingWarning; | ||
expect(error.message).toMatch(/already tagged/); | ||
} | ||
|
||
expect(ec2Mock).toHaveReceivedCommand(DescribeInstancesCommand); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import dbg from "debug"; | ||
import { | ||
CreateTagsCommand, | ||
DescribeInstancesCommand, | ||
EC2Client, | ||
} from "@aws-sdk/client-ec2"; | ||
import { TerminatingWarning } from "../lib/errors"; | ||
import { getConfiguration } from "../configuration"; | ||
import { tagsAsObject } from "../lib/aws-helpers"; | ||
import { tagNames } from "../lib/constants"; | ||
|
||
const debug = dbg("boxes:import"); | ||
|
||
type ImportOptions = { | ||
instanceId: string; | ||
boxId: string; | ||
overwrite: boolean; | ||
}; | ||
|
||
export async function importBox(options: ImportOptions): Promise<void> { | ||
const { instanceId, boxId, overwrite } = options; | ||
|
||
// Create an EC2 client. | ||
const { aws: awsConfig } = await getConfiguration(); | ||
const client = new EC2Client(awsConfig); | ||
|
||
// Get our AWS instances, we'll search for the instance and check for | ||
// conflicts. | ||
debug(`preparing to describe all instances...`); | ||
const response = await client.send(new DescribeInstancesCommand({})); | ||
if (!response || !response.Reservations) { | ||
throw new TerminatingWarning("Failed to query AWS for boxes/reservations"); | ||
} | ||
debug("...described successfully"); | ||
|
||
// Find the instance. If it doesn't exist, fail. | ||
const instance = response.Reservations?.flatMap((r) => r.Instances).find( | ||
(instance) => instance?.InstanceId === instanceId, | ||
); | ||
|
||
// If there is no instance with the provided instance id, fail. | ||
if (!instance) { | ||
throw new TerminatingWarning(`Instance with id '${instanceId}' not found`); | ||
} | ||
|
||
// If this instance already has a box id, but we have not chosen to | ||
// overwrite it, then fail. | ||
const tags = tagsAsObject(instance?.Tags); | ||
if (tags.hasOwnProperty(tagNames.boxId) && !overwrite) { | ||
throw new TerminatingWarning( | ||
`Instance '${instanceId}' is already tagged with box id '${ | ||
tags[tagNames.boxId] | ||
}`, | ||
); | ||
} | ||
|
||
// Get any volumes ids that we will tag. | ||
const volumeIds = | ||
instance?.BlockDeviceMappings?.map((bdm) => bdm.Ebs?.VolumeId).filter( | ||
(volumeId): volumeId is string => !!volumeId, | ||
) || []; | ||
|
||
// Send the 'start instances' command. Find the status of the starting | ||
// instance in the respose. | ||
debug( | ||
`preparing to tag instance ${instanceId} and volumes ${volumeIds} with ${tagNames.boxId}=${boxId}...`, | ||
); | ||
await client.send( | ||
new CreateTagsCommand({ | ||
Resources: [instanceId, ...volumeIds], | ||
Tags: [ | ||
{ | ||
Key: tagNames.boxId, | ||
Value: boxId, | ||
}, | ||
], | ||
}), | ||
); | ||
debug(`...complete`); | ||
} |
Oops, something went wrong.