Skip to content

Commit

Permalink
feat: Metadata Server Detection Configuration (#562)
Browse files Browse the repository at this point in the history
* feat: METADATA_SERVER_DETECTION

* Chore: `skipLibCheck`

Avoids a type issue with gax that requires a major version bump.

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* fix: docs

`compdoc` wants the babel plugin now

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

---------

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
danielbankhead and gcf-owl-bot[bot] authored Jun 28, 2023
1 parent 3eff5fb commit 8c7c715
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 18 deletions.
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",
"@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':
// 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;
}

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,
"rootDir": ".",
"outDir": "build"
},
Expand Down

0 comments on commit 8c7c715

Please sign in to comment.