diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 748464e..84db7ec 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -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 diff --git a/README.md b/README.md index e9c0d49..4bfd83e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index 004f5ae..529e8d0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index 47eb003..ec54ffd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; @@ -199,6 +213,30 @@ let cachedIsAvailableResponse: Promise | 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 @@ -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(); @@ -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'; diff --git a/test/index.test.ts b/test/index.test.ts index ebc4d9f..938d639 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -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(); @@ -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) @@ -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); diff --git a/tsconfig.json b/tsconfig.json index b7492c4..cca4310 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { "lib": ["es2018", "dom"], + "skipLibCheck": true, "rootDir": ".", "outDir": "build" },