From ac6799edb79cf58178c4841a2d1f70f6198af836 Mon Sep 17 00:00:00 2001 From: Siddharth Srivastava Date: Wed, 17 Dec 2025 00:19:52 +0000 Subject: [PATCH 1/4] fix(ec2-metadata-service): add configurable options for ttl and port precedence --- .../src/MetadataService.spec.ts | 125 +++++++++++++++++- .../src/MetadataService.ts | 29 +++- .../src/MetadataServiceOptions.ts | 9 ++ 3 files changed, 159 insertions(+), 4 deletions(-) diff --git a/packages/ec2-metadata-service/src/MetadataService.spec.ts b/packages/ec2-metadata-service/src/MetadataService.spec.ts index 964a2b269000..ddf55a51416b 100644 --- a/packages/ec2-metadata-service/src/MetadataService.spec.ts +++ b/packages/ec2-metadata-service/src/MetadataService.spec.ts @@ -326,7 +326,45 @@ describe("MetadataService Custom Ports", () => { await metadataService.fetchMetadataToken(); const requestArg = mockHandle.mock.calls[0][0]; - expect(requestArg.port).toBeUndefined(); + expect(requestArg.port).toBe(80); // default + expect(requestArg.hostname).toBe("169.254.169.254"); + }); + + it("should use explicit port option over endpoint URL port", async () => { + metadataService = new MetadataService({ + endpoint: "http://localhost:1338", + port: 9999, // Should override endpoint port + retries: 0, + }); + + const mockResponse = createMockResponse(200, "test-token-123"); + const mockHandle = vi.fn().mockResolvedValue(mockResponse); + + vi.mocked(NodeHttpHandler).mockImplementation(() => ({ handle: mockHandle } as any)); + + await metadataService.fetchMetadataToken(); + + const requestArg = mockHandle.mock.calls[0][0]; + expect(requestArg.port).toBe(9999); + expect(requestArg.hostname).toBe("localhost"); + }); + + it("should use explicit port option with default endpoint", async () => { + metadataService = new MetadataService({ + endpoint: "http://169.254.169.254", + port: 8080, + retries: 0, + }); + + const mockResponse = createMockResponse(200, "test-token-123"); + const mockHandle = vi.fn().mockResolvedValue(mockResponse); + + vi.mocked(NodeHttpHandler).mockImplementation(() => ({ handle: mockHandle } as any)); + + await metadataService.fetchMetadataToken(); + + const requestArg = mockHandle.mock.calls[0][0]; + expect(requestArg.port).toBe(8080); expect(requestArg.hostname).toBe("169.254.169.254"); }); }); @@ -395,3 +433,88 @@ describe("MetadataService Retry Configuration", () => { }); }); }); + +describe("MetadataService Token TTL Configuration", () => { + let metadataService: MetadataService; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const createMockResponse = (statusCode: number, body: string) => { + const stream = Readable.from([body]); + return { + response: { + statusCode, + body: stream, + headers: {}, + }, + }; + }; + + it("should use default tokenTtl of 21600 seconds", async () => { + metadataService = new MetadataService({ + endpoint: "http://169.254.169.254", + retries: 0, + }); + + const mockResponse = createMockResponse(200, "test-token-123"); + const mockHandle = vi.fn().mockResolvedValue(mockResponse); + + vi.mocked(NodeHttpHandler).mockImplementation(() => ({ handle: mockHandle } as any)); + + await metadataService.fetchMetadataToken(); + + const requestArg = mockHandle.mock.calls[0][0]; + expect(requestArg.headers["x-aws-ec2-metadata-token-ttl-seconds"]).toBe("21600"); + }); + + it("should use custom tokenTtl value", async () => { + metadataService = new MetadataService({ + endpoint: "http://169.254.169.254", + tokenTtl: 3600, // 1 hour + retries: 0, + }); + + const mockResponse = createMockResponse(200, "test-token-123"); + const mockHandle = vi.fn().mockResolvedValue(mockResponse); + + vi.mocked(NodeHttpHandler).mockImplementation(() => ({ handle: mockHandle } as any)); + + await metadataService.fetchMetadataToken(); + + const requestArg = mockHandle.mock.calls[0][0]; + expect(requestArg.headers["x-aws-ec2-metadata-token-ttl-seconds"]).toBe("3600"); + }); + + it("should validate tokenTtl as positive integer", () => { + expect(() => new MetadataService({ tokenTtl: -1 })).toThrow("tokenTtl must be a positive integer"); + expect(() => new MetadataService({ tokenTtl: 0 })).toThrow("tokenTtl must be a positive integer"); + expect(() => new MetadataService({ tokenTtl: 3.14 })).toThrow("tokenTtl must be a positive integer"); + }); + + it("should accept valid positive integer tokenTtl values", () => { + expect(() => new MetadataService({ tokenTtl: 1 })).not.toThrow(); + expect(() => new MetadataService({ tokenTtl: 3600 })).not.toThrow(); + expect(() => new MetadataService({ tokenTtl: 21600 })).not.toThrow(); + }); + + it("should convert tokenTtl to string in header", async () => { + metadataService = new MetadataService({ + endpoint: "http://169.254.169.254", + tokenTtl: 7200, + retries: 0, + }); + + const mockResponse = createMockResponse(200, "test-token-123"); + const mockHandle = vi.fn().mockResolvedValue(mockResponse); + + vi.mocked(NodeHttpHandler).mockImplementation(() => ({ handle: mockHandle } as any)); + + await metadataService.fetchMetadataToken(); + + const requestArg = mockHandle.mock.calls[0][0]; + expect(typeof requestArg.headers["x-aws-ec2-metadata-token-ttl-seconds"]).toBe("string"); + expect(requestArg.headers["x-aws-ec2-metadata-token-ttl-seconds"]).toBe("7200"); + }); +}); diff --git a/packages/ec2-metadata-service/src/MetadataService.ts b/packages/ec2-metadata-service/src/MetadataService.ts index 5e4270c21aa4..b3c268ee72e1 100644 --- a/packages/ec2-metadata-service/src/MetadataService.ts +++ b/packages/ec2-metadata-service/src/MetadataService.ts @@ -15,6 +15,9 @@ export class MetadataService { private config: Promise; private retries: number; private backoffFn: (numFailures: number) => Promise | number; + private tokenTtl: number; + // can be set explicitly, or extracted from endpoint, or use a default value (80). See `resolvePort()` below. + private port?: number; /** * Creates a new MetadataService object with a given set of options. @@ -34,6 +37,26 @@ export class MetadataService { this.disableFetchToken = options?.disableFetchToken || false; this.retries = options?.retries ?? 3; this.backoffFn = this.createBackoffFunction(options?.backoff); + this.tokenTtl = this.validateTokenTtl(options?.tokenTtl ?? 21600); + this.port = options?.port; + } + + private validateTokenTtl(tokenTtl: number): number { + if (!Number.isInteger(tokenTtl) || tokenTtl <= 0) { + throw new Error("tokenTtl must be a positive integer"); + } + return tokenTtl; + } + + private resolvePort(endpointUrl: URL): number | undefined { + // Priority: explicit port option > port from endpoint URL > default (80) + if (this.port !== undefined) { + return this.port; + } + if (endpointUrl.port) { + return parseInt(endpointUrl.port); + } + return 80; } private createBackoffFunction( @@ -129,7 +152,7 @@ export class MetadataService { hostname: endpointUrl.hostname, path: endpointUrl.pathname + path, protocol: endpointUrl.protocol, - port: endpointUrl.port ? parseInt(endpointUrl.port) : undefined, + port: this.resolvePort(endpointUrl), }); try { const { response } = await handler.handle(request, {} as HttpHandlerOptions); @@ -170,12 +193,12 @@ export class MetadataService { const tokenRequest = new HttpRequest({ method: "PUT", headers: { - "x-aws-ec2-metadata-token-ttl-seconds": "21600", // 6 hours; + "x-aws-ec2-metadata-token-ttl-seconds": String(this.tokenTtl), }, hostname: endpointUrl.hostname, path: "/latest/api/token", protocol: endpointUrl.protocol, - port: endpointUrl.port ? parseInt(endpointUrl.port) : undefined, + port: this.resolvePort(endpointUrl), }); try { const { response } = await handler.handle(tokenRequest, {} as HttpHandlerOptions); diff --git a/packages/ec2-metadata-service/src/MetadataServiceOptions.ts b/packages/ec2-metadata-service/src/MetadataServiceOptions.ts index 058a7aa57354..309d5bfb790a 100644 --- a/packages/ec2-metadata-service/src/MetadataServiceOptions.ts +++ b/packages/ec2-metadata-service/src/MetadataServiceOptions.ts @@ -37,4 +37,13 @@ export interface MetadataServiceOptions { * if the function returns a number, the number will be used as seconds duration to wait before the following retry attempt. */ backoff?: number | ((numFailures: number) => Promise | number); + /** + * the TTL of the token in seconds, defaulting to 21,600 seconds (6 hours) + */ + tokenTtl?: number; + /** + * the port for the endpoint, defaulting to 80. + * can also be provided as a part of the endpoint URL, though an explicit config value will take precedence. + */ + port?: number; } From a19d51d8c761a9b2d91317cc472725dae550e42f Mon Sep 17 00:00:00 2001 From: Siddharth Srivastava Date: Thu, 18 Dec 2025 19:32:22 +0000 Subject: [PATCH 2/4] fix(ec2-metadata-service): fix default for port as undefined for protocol based defaults --- packages/ec2-metadata-service/src/MetadataService.ts | 4 ++-- packages/ec2-metadata-service/src/MetadataServiceOptions.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ec2-metadata-service/src/MetadataService.ts b/packages/ec2-metadata-service/src/MetadataService.ts index b3c268ee72e1..f38e98f4c0e6 100644 --- a/packages/ec2-metadata-service/src/MetadataService.ts +++ b/packages/ec2-metadata-service/src/MetadataService.ts @@ -49,14 +49,14 @@ export class MetadataService { } private resolvePort(endpointUrl: URL): number | undefined { - // Priority: explicit port option > port from endpoint URL > default (80) + // Priority: explicit port option > port from endpoint URL > protocol default (undefined lets URL handle it) if (this.port !== undefined) { return this.port; } if (endpointUrl.port) { return parseInt(endpointUrl.port); } - return 80; + return undefined; } private createBackoffFunction( diff --git a/packages/ec2-metadata-service/src/MetadataServiceOptions.ts b/packages/ec2-metadata-service/src/MetadataServiceOptions.ts index 309d5bfb790a..22f786d8629b 100644 --- a/packages/ec2-metadata-service/src/MetadataServiceOptions.ts +++ b/packages/ec2-metadata-service/src/MetadataServiceOptions.ts @@ -42,7 +42,7 @@ export interface MetadataServiceOptions { */ tokenTtl?: number; /** - * the port for the endpoint, defaulting to 80. + * the port for the endpoint. If not specified, uses the default port for the protocol (80 for HTTP, 443 for HTTPS). * can also be provided as a part of the endpoint URL, though an explicit config value will take precedence. */ port?: number; From 8defe6d91e7e2dd6f5bd36d0155bc4d2037f7890 Mon Sep 17 00:00:00 2001 From: Siddharth Srivastava Date: Thu, 18 Dec 2025 19:37:11 +0000 Subject: [PATCH 3/4] test(ec2-metadata-service): update test for port default --- packages/ec2-metadata-service/src/MetadataService.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ec2-metadata-service/src/MetadataService.spec.ts b/packages/ec2-metadata-service/src/MetadataService.spec.ts index ddf55a51416b..1a984b87f3a6 100644 --- a/packages/ec2-metadata-service/src/MetadataService.spec.ts +++ b/packages/ec2-metadata-service/src/MetadataService.spec.ts @@ -326,7 +326,7 @@ describe("MetadataService Custom Ports", () => { await metadataService.fetchMetadataToken(); const requestArg = mockHandle.mock.calls[0][0]; - expect(requestArg.port).toBe(80); // default + expect(requestArg.port).toBe(undefined); // protocol default expect(requestArg.hostname).toBe("169.254.169.254"); }); From 269002c6bf5f778bd690db83367b4c38fff3d50f Mon Sep 17 00:00:00 2001 From: Siddharth Srivastava Date: Fri, 19 Dec 2025 00:13:59 +0000 Subject: [PATCH 4/4] docs(ec2-metadata-service): minor doc update for port default --- packages/ec2-metadata-service/src/MetadataService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ec2-metadata-service/src/MetadataService.ts b/packages/ec2-metadata-service/src/MetadataService.ts index f38e98f4c0e6..67e15f3cb17c 100644 --- a/packages/ec2-metadata-service/src/MetadataService.ts +++ b/packages/ec2-metadata-service/src/MetadataService.ts @@ -16,7 +16,7 @@ export class MetadataService { private retries: number; private backoffFn: (numFailures: number) => Promise | number; private tokenTtl: number; - // can be set explicitly, or extracted from endpoint, or use a default value (80). See `resolvePort()` below. + // can be set explicitly, or extracted from endpoint, or use a default value. See `resolvePort()` below. private port?: number; /**