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: Metadata Server Detection Configuration #562

Merged
merged 5 commits into from
Jun 28, 2023
Merged
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
21 changes: 15 additions & 6 deletions .readme-partials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,20 @@ body: |-

### Environment variables

* GCE_METADATA_HOST: provide an alternate host or IP to perform lookup against (useful, for example, you're connecting through a custom proxy server).
* `GCE_METADATA_HOST`: provide an alternate host or IP to perform lookup against (useful, for example, you're connecting through a custom proxy server).

For example:
```
export GCE_METADATA_HOST = '169.254.169.254'
```
For example:
```
export GCE_METADATA_HOST = '169.254.169.254'
```

* `DETECT_GCP_RETRIES`: number representing number of retries that should be attempted on metadata lookup.

* `DEBUG_AUTH`: emit debugging logs

* `METADATA_SERVER_DETECTION`: configure desired metadata server availability check behavior.

* DETECT_GCP_RETRIES: number representing number of retries that should be attempted on metadata lookup.
* `assume-present`: don't try to ping the metadata server, but assume it's present
* `none`: don't try to ping the metadata server, but don't try to use it either
* `bios-only`: treat the result of a BIOS probe as canonical (don't fall back to pinging)
* `ping-only`: skip the BIOS probe, and go straight to pinging
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,23 @@ console.log(id.toString()) // ... 4520031799277581759

### Environment variables

* GCE_METADATA_HOST: provide an alternate host or IP to perform lookup against (useful, for example, you're connecting through a custom proxy server).
* `GCE_METADATA_HOST`: provide an alternate host or IP to perform lookup against (useful, for example, you're connecting through a custom proxy server).

For example:
```
export GCE_METADATA_HOST = '169.254.169.254'
```
For example:
```
export GCE_METADATA_HOST = '169.254.169.254'
```

* `DETECT_GCP_RETRIES`: number representing number of retries that should be attempted on metadata lookup.

* `DEBUG_AUTH`: emit debugging logs

* `METADATA_SERVER_DETECTION`: configure desired metadata server availability check behavior.

* DETECT_GCP_RETRIES: number representing number of retries that should be attempted on metadata lookup.
* `assume-present`: don't try to ping the metadata server, but assume it's present
* `none`: don't try to ping the metadata server, but don't try to use it either
* `bios-only`: treat the result of a BIOS probe as canonical (don't fall back to pinging)
* `ping-only`: skip the BIOS probe, and go straight to pinging


## Samples
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"json-bigint": "^1.0.0"
},
"devDependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
Copy link
Contributor

@ddelgrosso1 ddelgrosso1 Jun 23, 2023

Choose a reason for hiding this comment

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

I see this was added but is it used someplace or did I misunderstand?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, for some reason compdoc wants the babel plugin now, otherwise it’ll throw and fail the docs check (and linkinator would also complain).

Copy link
Member Author

Choose a reason for hiding this comment

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

Update: It's a Node 14+ requirement

Copy link
Member Author

Choose a reason for hiding this comment

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

Tracking issue: #563

"@compodoc/compodoc": "^1.1.10",
"@google-cloud/functions": "^2.0.0",
"@types/json-bigint": "^1.0.0",
Expand Down
60 changes: 54 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ export const HEADER_NAME = 'Metadata-Flavor';
export const HEADER_VALUE = 'Google';
export const HEADERS = Object.freeze({[HEADER_NAME]: HEADER_VALUE});

/**
* Metadata server detection override options.
*
* Available via `process.env.METADATA_SERVER_DETECTION`.
*/
export const METADATA_SERVER_DETECTION = Object.freeze({
'assume-present':
"don't try to ping the metadata server, but assume it's present",
none: "don't try to ping the metadata server, but don't try to use it either",
'bios-only':
"treat the result of a BIOS probe as canonical (don't fall back to pinging)",
'ping-only': 'skip the BIOS probe, and go straight to pinging',
});

export interface Options {
params?: {[index: string]: string};
property?: string;
Expand Down Expand Up @@ -199,6 +213,30 @@ let cachedIsAvailableResponse: Promise<boolean> | undefined;
* Determine if the metadata server is currently available.
*/
export async function isAvailable() {
if (process.env.METADATA_SERVER_DETECTION) {
const value =
process.env.METADATA_SERVER_DETECTION.trim().toLocaleLowerCase();

if (!(value in METADATA_SERVER_DETECTION)) {
throw new RangeError(
`Unknown \`METADATA_SERVER_DETECTION\` env variable. Got \`${value}\`, but it should be \`${Object.keys(
METADATA_SERVER_DETECTION
).join('`, `')}\`, or unset`
);
}

switch (value as keyof typeof METADATA_SERVER_DETECTION) {
case 'assume-present':
return true;
case 'none':
return false;
case 'bios-only':
return getGCPResidency();
case 'ping-only':
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we discussed but I forgot.. we don't want to have a default where both ping and bios happen sequentially?

Copy link
Member

Choose a reason for hiding this comment

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

or default in this case is null and also ping?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, the default in this case is null and ping - I’m open to changing it.

// continue, we want to ping the server
}
}

try {
// If a user is instantiating several GCP libraries at the same time,
// this may result in multiple calls to isAvailable(), to detect the
Expand Down Expand Up @@ -271,11 +309,26 @@ export function resetIsAvailableCache() {
*/
export let gcpResidencyCache: boolean | null = null;

/**
* Detects GCP Residency.
* Caches results to reduce costs for subsequent calls.
*
* @see setGCPResidency for setting
*/
export function getGCPResidency(): boolean {
if (gcpResidencyCache === null) {
setGCPResidency();
}

return gcpResidencyCache!;
}

/**
* Sets the detected GCP Residency.
* Useful for forcing metadata server detection behavior.
*
* Set `null` to autodetect the environment (default behavior).
* @see getGCPResidency for getting
*/
export function setGCPResidency(value: boolean | null = null) {
gcpResidencyCache = value !== null ? value : detectGCPResidency();
Expand All @@ -291,12 +344,7 @@ export function setGCPResidency(value: boolean | null = null) {
* @returns {number} a request timeout duration in milliseconds.
*/
export function requestTimeout(): number {
// Detecting the residency can be resource-intensive. Let's cache the result.
if (gcpResidencyCache === null) {
gcpResidencyCache = detectGCPResidency();
}

return gcpResidencyCache ? 0 : 3000;
return getGCPResidency() ? 0 : 3000;
Copy link
Member

Choose a reason for hiding this comment

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

Curious what timeout it is and why exactly 0?

Copy link
Member Author

Choose a reason for hiding this comment

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

The timeout is used for availability checking; 0 (indefinite) for GCE and up to 3 secs for other platforms.

}

export * from './gcp-residency';
106 changes: 106 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ describe('unit test', () => {
// expected test outcome.
delete process.env.GCE_METADATA_HOST;
delete process.env.GCE_METADATA_IP;
delete process.env.METADATA_SERVER_DETECTION;

gcp.resetIsAvailableCache();

sandbox = createSandbox();
Expand Down Expand Up @@ -261,6 +263,95 @@ describe('unit test', () => {
});
}

describe('METADATA_SERVER_DETECTION', () => {
it('should respect `assume-present`', async () => {
process.env.METADATA_SERVER_DETECTION = 'assume-present';

// if this is called, this the test would fail.
const scope = nock(HOST);

const isGCE = await gcp.isAvailable();
assert.strictEqual(isGCE, true);

scope.done();
});

it('should respect `bios-only` (residency = true)', async () => {
process.env.METADATA_SERVER_DETECTION = 'bios-only';

// if this is called, this the test would fail.
const scope = nock(HOST);

gcp.setGCPResidency(true);
const isGCE = await gcp.isAvailable();
assert.strictEqual(isGCE, true);

scope.done();
});

it('should respect `bios-only` (residency = false)', async () => {
process.env.METADATA_SERVER_DETECTION = 'bios-only';

// if either are called, this the test would fail.
nock(HOST).get(`${PATH}/${TYPE}`).reply(200, {}, HEADERS);
nock(SECONDARY_HOST).get(`${PATH}/${TYPE}`).reply(200, {}, HEADERS);

gcp.setGCPResidency(false);
const isGCE = await gcp.isAvailable();
assert.strictEqual(isGCE, false);

nock.cleanAll();
});

it('should respect `none`', async () => {
process.env.METADATA_SERVER_DETECTION = 'none';

// if either are called, this the test would fail.
nock(HOST).get(`${PATH}/${TYPE}`).reply(200, {}, HEADERS);
nock(SECONDARY_HOST).get(`${PATH}/${TYPE}`).reply(200, {}, HEADERS);

// if this is referenced, this test would fail.
gcp.setGCPResidency(true);

const isGCE = await gcp.isAvailable();
assert.strictEqual(isGCE, false);
});

it('should respect `ping-only`', async () => {
process.env.METADATA_SERVER_DETECTION = 'ping-only';

gcp.resetIsAvailableCache();
nock(HOST).get(`${PATH}/${TYPE}`).reply(200, {}, HEADERS);
nock(SECONDARY_HOST).get(`${PATH}/${TYPE}`).reply(200, {}, HEADERS);

// if this is referenced, this test would fail.
gcp.setGCPResidency(false);

const isGCE = await gcp.isAvailable();
assert.strictEqual(isGCE, true);

nock.cleanAll();
});

it('should ignore spaces and capitalization', async () => {
process.env.METADATA_SERVER_DETECTION = ' ASSUME-present\t';

// if this is called, this the test would fail.
const scope = nock(HOST);

const isGCE = await gcp.isAvailable();
assert.strictEqual(isGCE, true);

scope.done();
});

it('should throw on unknown values', async () => {
process.env.METADATA_SERVER_DETECTION = 'abc';

await assert.rejects(gcp.isAvailable, RangeError);
});
});

it('should report isGCE if primary server returns 500 followed by 200', async () => {
const secondary = secondaryHostRequest(500);
const primary = nock(HOST)
Expand Down Expand Up @@ -496,6 +587,21 @@ describe('unit test', () => {
assert.strictEqual(isGCE, false);
});

describe('getGCPResidency', () => {
it('should set and use `gcpResidencyCache`', () => {
gcp.setGCPResidency(false);
assert.equal(gcp.getGCPResidency(), false);
assert.equal(gcp.gcpResidencyCache, false);

gcp.setGCPResidency(true);
assert.equal(gcp.getGCPResidency(), true);
assert.equal(gcp.gcpResidencyCache, true);

gcp.setGCPResidency(null);
assert.equal(gcp.getGCPResidency(), gcp.gcpResidencyCache);
});
});

describe('setGCPResidency', () => {
it('should set `gcpResidencyCache`', () => {
gcp.setGCPResidency(true);
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "./node_modules/gts/tsconfig-google.json",
"compilerOptions": {
"lib": ["es2018", "dom"],
"skipLibCheck": true,
Copy link
Member Author

Choose a reason for hiding this comment

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

Will create a follow-up issue to revert this in the next major (should be soon).

Copy link
Member Author

Choose a reason for hiding this comment

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

Background: https://www.typescriptlang.org/tsconfig#skipLibCheck

There's a broken type issue that would require a major version bump to resolve.

Copy link
Contributor

Choose a reason for hiding this comment

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

I too got bitten by this broken type issue

Copy link
Member Author

Choose a reason for hiding this comment

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

Tracking issue: #564

"rootDir": ".",
"outDir": "build"
},
Expand Down