Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ec2-metadata-service): implement utils for ec2 metadata service (imds) #5796

Merged
merged 22 commits into from
Mar 18, 2024
Merged
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c4f98d7
feat(ec2-imds-utils): creating dir and interface for metadata service
siddsriv Feb 14, 2024
9db86bd
feat(ec2-metadata-service): adding initial request method and some re…
siddsriv Feb 19, 2024
386c66a
feat(ec2-metadata-service): adding fetchMetadataToken function
siddsriv Feb 19, 2024
6999f36
fix(ec2-metadata-service): handle direct text responses only
siddsriv Feb 19, 2024
0ddd80c
feat(ec2-metadata-service): adding util functions
siddsriv Feb 21, 2024
e52d7a9
chore(ec2-metadata-service): fix return format for consistency
siddsriv Feb 22, 2024
4e0104b
fix(ec2-metadata-service): modifications/refactor for util functions …
siddsriv Feb 23, 2024
b7a0d38
fix(ec2-metadata-service): fix fetchMetadataToken path
siddsriv Feb 27, 2024
9e98b5c
feat(ec2-metadata-service): adding requestWithToken and fetchImdsJson
siddsriv Feb 29, 2024
d64fb6e
chore(ec2-metadata-service): adding tsconfig files
siddsriv Feb 29, 2024
6358dcc
chore(ec2-metadata-service): add index.ts
siddsriv Feb 29, 2024
d7b01de
chore(ec2-metadata-service): rename function to get host
siddsriv Mar 1, 2024
a3cbb01
test(ec2-metadata-service): initial e2e test expts
siddsriv Mar 1, 2024
8ab2c63
feat(ec2-metadata-service): add withToken flag for request and delete…
siddsriv Mar 6, 2024
9bcbe16
test(ec2-metadata-service): built, test to confirm req/resp
siddsriv Mar 6, 2024
b52d5c5
chore(ec2-metadata-service): add credential-provider-imds dep
siddsriv Mar 8, 2024
cdd2e4e
feat(ec2-metadata-service): adding config selectors
siddsriv Mar 8, 2024
9852698
fix(ec2-metadata-service): fix typechecking for imdsv1 disable select…
siddsriv Mar 15, 2024
a8fd616
feat(ec2-metadata-service): add IMDSv1 fallback handling
siddsriv Mar 15, 2024
f0e4eb3
test(ec2-metadata-service): tests for IMDSv1 fallback cases
siddsriv Mar 15, 2024
4f37093
chore(ec2-metadata-service): add README
siddsriv Mar 18, 2024
79d7a6b
chore(ec2-metadata-service): update workspace version
siddsriv Mar 18, 2024
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
Prev Previous commit
Next Next commit
feat(ec2-metadata-service): adding config selectors
siddsriv committed Mar 8, 2024

Verified

This commit was signed with the committer’s verified signature.
commit cdd2e4ec0239531d204139fe30fb376d2b2b957d
2 changes: 1 addition & 1 deletion packages/ec2-metadata-service/package.json
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@
"@smithy/util-stream": "^2.1.1",
"@aws-sdk/types": "3.515.0",
"@aws-sdk/protocol-http": "3.374.0",
"@smithy/credential-provider-imds": "2.2.6"
"@smithy/node-config-provider": "2.2.5"
},
"devDependencies": {
"@tsconfig/recommended": "1.0.1",
57 changes: 57 additions & 0 deletions packages/ec2-metadata-service/src/ConfigLoaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { LoadedConfigSelectors } from "@smithy/node-config-provider";
import { EndpointMode } from "./EndpointMode";

/**
* @internal
*/
export const ENV_ENDPOINT_NAME = "AWS_EC2_METADATA_SERVICE_ENDPOINT";
/**
* @internal
*/
export const CONFIG_ENDPOINT_NAME = "ec2_metadata_service_endpoint";

/**
* @internal
*/
export const ENDPOINT_SELECTORS: LoadedConfigSelectors<string | undefined> = {
environmentVariableSelector: (env) => env[ENV_ENDPOINT_NAME],
configFileSelector: (profile) => profile[CONFIG_ENDPOINT_NAME],
default: undefined,
};

/**
* @internal
*/
export const ENV_ENDPOINT_MODE_NAME = "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE";
/**
* @internal
*/
export const CONFIG_ENDPOINT_MODE_NAME = "ec2_metadata_service_endpoint_mode";

/**
* @internal
*/
export const ENDPOINT_MODE_SELECTORS: LoadedConfigSelectors<string | undefined> = {
environmentVariableSelector: (env) => env[ENV_ENDPOINT_MODE_NAME],
configFileSelector: (profile) => profile[CONFIG_ENDPOINT_MODE_NAME],
default: EndpointMode.IPv4,
};

/**
* @internal
*/
export const AWS_EC2_METADATA_V1_DISABLED = "AWS_EC2_METADATA_V1_DISABLED";
/**
* @internal
*/
export const PROFILE_AWS_EC2_METADATA_V1_DISABLED = "ec2_metadata_v1_disabled";
/**
* @internal
*/
export const IMDSv1_DISABLED_SELECTORS: LoadedConfigSelectors<boolean | undefined> = {
environmentVariableSelector: (env) =>
env[AWS_EC2_METADATA_V1_DISABLED] && env[AWS_EC2_METADATA_V1_DISABLED] !== "false",
configFileSelector: (profile) =>
profile[PROFILE_AWS_EC2_METADATA_V1_DISABLED] && profile[PROFILE_AWS_EC2_METADATA_V1_DISABLED] !== "false",
default: false,
};
7 changes: 7 additions & 0 deletions packages/ec2-metadata-service/src/EndpointMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @internal
*/
export enum EndpointMode {
IPv4 = "IPv4",
IPv6 = "IPv6",
}
47 changes: 35 additions & 12 deletions packages/ec2-metadata-service/src/MetadataService.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import { MetadataService } from "./MetadataService";
import { fromInstanceMetadata } from "@aws-sdk/credential-providers";

describe("MetadataService E2E Tests", () => {
let metadataService;
const provider = fromInstanceMetadata({ timeout: 1000, maxRetries: 0 });
let metadataServiceAvailable;

beforeAll(() => {
beforeAll(async () => {
try {
await provider();
metadataServiceAvailable = true;
} catch (err) {
metadataServiceAvailable = false;
}
console.log("Metadata Service availability: ", metadataServiceAvailable);
metadataService = new MetadataService({});
console.log("IMDS Endpoint: ", metadataService.endpoint);
});

it("should fetch metadata token successfully", async () => {
// This test will only pass if run on an EC2 instance or an environment with access to the EC2 Metadata Service
if (!metadataServiceAvailable) {
return;
}
const token = await metadataService.fetchMetadataToken();
expect(token).toBeDefined();
expect(typeof token).toBe("string");
expect(token.length).toBeGreaterThan(0);
console.log(token);
});

it("should fetch instance ID successfully", async () => {
const instanceId = await metadataService.request("/latest/meta-data/instance-id", {}, false);
expect(instanceId).toBeDefined();
expect(typeof instanceId).toBe("string");
expect(instanceId.length).toBeGreaterThan(0);
console.log(instanceId);
if (!metadataServiceAvailable) {
return;
}
const metadata = await metadataService.request("/latest/meta-data/", {}, false);
expect(metadata).toBeDefined();
expect(typeof metadata).toBe("string");
expect(metadata.length).toBeGreaterThan(0);
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 instance ID successfully with token", async () => {
const instanceId = await metadataService.request("/latest/meta-data/instance-id", {}, true);
expect(instanceId).toBeDefined();
expect(typeof instanceId).toBe("string");
expect(instanceId.length).toBeGreaterThan(0);
console.log(instanceId);
if (!metadataServiceAvailable) {
return;
}
const metadata = await metadataService.request("/latest/meta-data/", {}, true);
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/");
});
});
66 changes: 34 additions & 32 deletions packages/ec2-metadata-service/src/MetadataService.ts
Original file line number Diff line number Diff line change
@@ -2,61 +2,58 @@ import { HttpRequest } from "@aws-sdk/protocol-http";
import { HttpHandlerOptions } from "@aws-sdk/types";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { sdkStreamMixin } from "@smithy/util-stream";

import { Endpoint } from "./Endpoint";
import { loadConfig } from "@smithy/node-config-provider";
import { ENDPOINT_SELECTORS, IMDSv1_DISABLED_SELECTORS } from "./ConfigLoaders";
import { MetadataServiceOptions } from "./MetadataServiceOptions";

/**
* @public
*/
export class MetadataService {
endpoint: string;
httpOptions: {
timeout: number;
};
maxRetries: number;
retryDelayOptions: any;
ec2MetadataV1Disabled: boolean;
profile: string;
filename: string;

private disableFetchToken: boolean;
private config: Promise<MetadataServiceOptions>;
/**
siddsriv marked this conversation as resolved.
Show resolved Hide resolved
* Creates a new MetadataService object with a given set of options.
*/
constructor(options?: MetadataServiceOptions) {
options = options || {};
this.endpoint = options.endpoint ? options.endpoint : Endpoint.IPv4;
this.httpOptions = {
timeout: options?.httpOptions?.timeout || 0,
};
this.maxRetries = options?.maxRetries || 3; // Assuming a default of 3 retries if not specified
this.retryDelayOptions = options?.retryDelayOptions || {};
this.ec2MetadataV1Disabled = options?.ec2MetadataV1Disabled || false;
this.profile = options?.profile || "";
this.filename = options?.filename || "";
constructor(options: MetadataServiceOptions = {}) {
this.config = (async () => {
const profile = options?.profile || process.env.AWS_PROFILE;
return {
endpoint: options.endpoint ?? (await loadConfig(ENDPOINT_SELECTORS, { profile })()),
httpOptions: {
timeout: options?.httpOptions?.timeout || 0,
},
ec2MetadataV1Disabled:
options?.ec2MetadataV1Disabled ?? (await loadConfig(IMDSv1_DISABLED_SELECTORS, { profile })()),
};
})();
this.disableFetchToken = options?.disableFetchToken || false;
}

async request(
path: string,
options: { method?: string; headers?: Record<string, string> },
withToken?: boolean
): Promise<string> {
let header = options.headers || {}; // Using provided headers or default to an empty object
const { endpoint, ec2MetadataV1Disabled } = await this.config;
if (this.disableFetchToken && !withToken && ec2MetadataV1Disabled) {
throw new Error(
"In IMDSv1 fallback mode and ec2MetadataV1Disabled option is set to true, no request can be made."
);
}
const handler = new NodeHttpHandler();
const endpointUrl = new URL(endpoint);
const headers = options.headers || {}; // Using provided headers or default to an empty object
/**
* Make request with token.
* Note that making the request call with token will result in an additional request to fetch the token.
*/
if (withToken) {
header = {
...options?.headers,
"X-aws-ec2-metadata-token": await this.fetchMetadataToken(),
};
headers["x-aws-ec2-metadata-token"] = await this.fetchMetadataToken();
}
const handler = new NodeHttpHandler();
const endpointUrl = new URL(this.endpoint);
const request = new HttpRequest({
method: options.method || "GET", // Default to GET if no method is specified
headers: header,
headers: headers,
hostname: endpointUrl.hostname,
path: endpointUrl.pathname + path,
protocol: endpointUrl.protocol,
@@ -75,9 +72,14 @@ export class MetadataService {
}

async fetchMetadataToken(): Promise<string> {
/**
* Refer: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html
*/

// Define the request to fetch the metadata token
const { endpoint } = await this.config;
const handler = new NodeHttpHandler();
const endpointUrl = new URL(this.endpoint);
const endpointUrl = new URL(endpoint);
const tokenRequest = new HttpRequest({
method: "PUT",
headers: {
18 changes: 5 additions & 13 deletions packages/ec2-metadata-service/src/MetadataServiceOptions.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @public
*/
export interface MetadataServiceOptions {
/**
* the endpoint of the instance metadata service.
@@ -12,27 +15,16 @@ export interface MetadataServiceOptions {
*/
timeout?: number;
};
/**
* the maximum number of retries to perform for timeout errors.
*/
maxRetries?: number;
/**
* A set of options to configure the retry delay on retryable errors.
*/
retryDelayOptions?: any;

/**
* Prevent IMDSv1 fallback.
*/
ec2MetadataV1Disabled?: boolean;

/**
* profile name to check for IMDSv1 settings.
*/
profile?: string;

/**
* optional file from which to to get config.
* when true, metadata service will not fetch token
*/
filename?: string;
disableFetchToken?: false;
}