From 28da9e6e31d3c1630adb0f035fe0ec81b9467ba7 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Fri, 15 Mar 2024 18:26:24 +0000 Subject: [PATCH] test(ec2-metadata-service): tests for IMDSv1 fallback cases --- packages/ec2-metadata-service/package.json | 3 +- .../src/MetadataService.e2e.spec.ts | 51 ++++++++++++++++--- .../src/MetadataService.ts | 26 +++++----- .../src/MetadataServiceOptions.d.ts | 2 +- 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/packages/ec2-metadata-service/package.json b/packages/ec2-metadata-service/package.json index faac7a962f923..c0494cf4d0efa 100644 --- a/packages/ec2-metadata-service/package.json +++ b/packages/ec2-metadata-service/package.json @@ -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" diff --git a/packages/ec2-metadata-service/src/MetadataService.e2e.spec.ts b/packages/ec2-metadata-service/src/MetadataService.e2e.spec.ts index dc09d79996e8d..1824a4c0e2474 100644 --- a/packages/ec2-metadata-service/src/MetadataService.e2e.spec.ts +++ b/packages/ec2-metadata-service/src/MetadataService.e2e.spec.ts @@ -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 }); @@ -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 () => { @@ -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); @@ -43,11 +59,15 @@ 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()); @@ -55,4 +75,23 @@ describe("MetadataService E2E Tests", () => { 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/"); + } + }); }); diff --git a/packages/ec2-metadata-service/src/MetadataService.ts b/packages/ec2-metadata-service/src/MetadataService.ts index fb17a0637d77e..5bb2035cdad8f 100644 --- a/packages/ec2-metadata-service/src/MetadataService.ts +++ b/packages/ec2-metadata-service/src/MetadataService.ts @@ -31,19 +31,11 @@ export class MetadataService { this.disableFetchToken = options?.disableFetchToken || false; } - async request( - path: string, - options: { method?: string; headers?: Record }, - withToken?: boolean // withToken should be set to true, if the request is to be made in accordance with IMDSv2 - ): Promise { + async request(path: string, options: { method?: string; headers?: Record }): Promise { 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 @@ -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) { @@ -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, @@ -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: { @@ -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}`); } } diff --git a/packages/ec2-metadata-service/src/MetadataServiceOptions.d.ts b/packages/ec2-metadata-service/src/MetadataServiceOptions.d.ts index 9ab92a811ca62..49659abc34bbc 100644 --- a/packages/ec2-metadata-service/src/MetadataServiceOptions.d.ts +++ b/packages/ec2-metadata-service/src/MetadataServiceOptions.d.ts @@ -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; }