Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 124 additions & 1 deletion packages/ec2-metadata-service/src/MetadataService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Expand Down Expand Up @@ -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");
});
});
29 changes: 26 additions & 3 deletions packages/ec2-metadata-service/src/MetadataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export class MetadataService {
private config: Promise<MetadataServiceOptions>;
private retries: number;
private backoffFn: (numFailures: number) => Promise<void> | 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.
Expand All @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions packages/ec2-metadata-service/src/MetadataServiceOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> | 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it default to undefined or the default port of the url's protocol?

* can also be provided as a part of the endpoint URL, though an explicit config value will take precedence.
*/
port?: number;
}
Loading