Skip to content

Commit

Permalink
test(ec2-metadata-service): tests for IMDSv1 fallback cases
Browse files Browse the repository at this point in the history
  • Loading branch information
siddsriv committed Mar 15, 2024
1 parent a8fd616 commit 28da9e6
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 22 deletions.
3 changes: 2 additions & 1 deletion packages/ec2-metadata-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"concurrently": "7.0.0",
"downlevel-dts": "0.10.1",
"rimraf": "3.0.2",
"typescript": "~4.9.5"
"typescript": "~4.9.5",
"@aws-sdk/credential-providers": "3.533.0"
},
"engines": {
"node": ">=14.0.0"
Expand Down
51 changes: 45 additions & 6 deletions packages/ec2-metadata-service/src/MetadataService.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MetadataService } from "./MetadataService";
import { fromInstanceMetadata } from "@aws-sdk/credential-providers";

import { MetadataService } from "./MetadataService";

describe("MetadataService E2E Tests", () => {
let metadataService;
const provider = fromInstanceMetadata({ timeout: 1000, maxRetries: 0 });
Expand All @@ -15,7 +16,8 @@ describe("MetadataService E2E Tests", () => {
}
console.log("Metadata Service availability: ", metadataServiceAvailable);
metadataService = new MetadataService({});
console.log("IMDS Endpoint: ", metadataService.endpoint);
const config = await metadataService.config;
console.log("IMDS Endpoint: ", config.endpoint);
});

it("should fetch metadata token successfully", async () => {
Expand All @@ -29,11 +31,25 @@ describe("MetadataService E2E Tests", () => {
expect(token.length).toBeGreaterThan(0);
});

it("should fetch instance ID successfully", async () => {
it("should fetch metadata successfully with token", async () => {
if (!metadataServiceAvailable) {
return;
}
const metadata = await metadataService.request("/latest/meta-data/", {});
expect(metadata).toBeDefined();
expect(typeof metadata).toBe("string");
const lines = metadata.split("\n").map((line) => line.trim());
expect(lines.length).toBeGreaterThan(5);
expect(lines).toContain("instance-id");
expect(lines).toContain("services/");
});

it("should fetch metadata successfully (without token -- disableFetchToken set to true)", async () => {
if (!metadataServiceAvailable) {
return;
}
const metadata = await metadataService.request("/latest/meta-data/", {}, false);
metadataService.disableFetchToken = true; // make request without token
const metadata = await metadataService.request("/latest/meta-data/", {});
expect(metadata).toBeDefined();
expect(typeof metadata).toBe("string");
expect(metadata.length).toBeGreaterThan(0);
Expand All @@ -43,16 +59,39 @@ describe("MetadataService E2E Tests", () => {
expect(lines).toContain("services/");
});

it("should fetch instance ID successfully with token", async () => {
it("should handle TimeoutError by falling back to IMDSv1", async () => {
if (!metadataServiceAvailable) {
return;
}
const metadata = await metadataService.request("/latest/meta-data/", {}, true);
jest.spyOn(metadataService, "fetchMetadataToken").mockImplementation(async () => {
throw { name: "TimeoutError" }; // Simulating TimeoutError
});
// Attempt to fetch metadata, expecting IMDSv1 fallback (request without token)
const metadata = await metadataService.request("/latest/meta-data/", {});
expect(metadata).toBeDefined();
expect(typeof metadata).toBe("string");
const lines = metadata.split("\n").map((line) => line.trim());
expect(lines.length).toBeGreaterThan(5);
expect(lines).toContain("instance-id");
expect(lines).toContain("services/");
});

it("should handle specific error codes by falling back to IMDSv1", async () => {
if (!metadataServiceAvailable) {
return;
}
const httpErrors = [403, 404, 405];
for (const errorCode of httpErrors) {
jest.spyOn(metadataService, "fetchMetadataToken").mockImplementationOnce(async () => {
throw { statusCode: errorCode };
});
const metadata = await metadataService.request("/latest/meta-data/", {});
expect(metadata).toBeDefined();
expect(typeof metadata).toBe("string");
const lines = metadata.split("\n").map((line) => line.trim());
expect(lines.length).toBeGreaterThan(5);
expect(lines).toContain("instance-id");
expect(lines).toContain("services/");
}
});
});
26 changes: 12 additions & 14 deletions packages/ec2-metadata-service/src/MetadataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,11 @@ export class MetadataService {
this.disableFetchToken = options?.disableFetchToken || false;
}

async request(
path: string,
options: { method?: string; headers?: Record<string, string> },
withToken?: boolean // withToken should be set to true, if the request is to be made in accordance with IMDSv2
): Promise<string> {
async request(path: string, options: { method?: string; headers?: Record<string, string> }): Promise<string> {
const { endpoint, ec2MetadataV1Disabled } = await this.config;
const handler = new NodeHttpHandler();
const endpointUrl = new URL(endpoint);
const endpointUrl = new URL(endpoint!);
const headers = options.headers || {};

if (this.disableFetchToken && withToken) {
throw new Error("The disableFetchToken option and the withToken argument are both set to true.");
}
/**
* If IMDSv1 is disabled and disableFetchToken is true, throw an error
* Refer: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
Expand All @@ -52,10 +44,10 @@ export class MetadataService {
throw new Error("IMDSv1 is disabled and fetching token is disabled, cannot make the request.");
}
/**
* Make request with token if disableFetchToken is not true and withToken is true.
* Make request with token if disableFetchToken is not true (IMDSv2).
* Note that making the request call with token will result in an additional request to fetch the token.
*/
if (withToken && !this.disableFetchToken) {
if (!this.disableFetchToken) {
try {
headers["x-aws-ec2-metadata-token"] = await this.fetchMetadataToken();
} catch (err) {
Expand All @@ -65,7 +57,7 @@ export class MetadataService {
}
// If token fetch fails and IMDSv1 is not disabled, proceed without token (IMDSv1 fallback)
}
}
} // else, IMDSv1 fallback mode
const request = new HttpRequest({
method: options.method || "GET", // Default to GET if no method is specified
headers: headers,
Expand All @@ -92,7 +84,7 @@ export class MetadataService {
*/
const { endpoint } = await this.config;
const handler = new NodeHttpHandler();
const endpointUrl = new URL(endpoint);
const endpointUrl = new URL(endpoint!);
const tokenRequest = new HttpRequest({
method: "PUT",
headers: {
Expand All @@ -111,6 +103,12 @@ export class MetadataService {
throw new Error(`Failed to fetch metadata token with status code ${response.statusCode}`);
}
} catch (error) {
if (error?.statusCode === 400) {
throw new Error(`Error fetching metadata token: ${error}`);
} else if (error.message === "TimeoutError" || [403, 404, 405].includes(error.statusCode)) {
this.disableFetchToken = true; // as per JSv2 and fromInstanceMetadata implementations
throw new Error(`Error fetching metadata token: ${error}. disableFetchToken is enabled`);
}
throw new Error(`Error fetching metadata token: ${error}`);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface MetadataServiceOptions {
*/
profile?: string;
/**
* when true, metadata service will not fetch token
* when true, metadata service will not fetch token, which indicates usage of IMDSv1
*/
disableFetchToken?: false;
}

0 comments on commit 28da9e6

Please sign in to comment.