From 46c134a16c1cd70b41ac03b08a7d22f6e32671d3 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 16 Aug 2022 16:24:00 -0700 Subject: [PATCH 1/9] feat: Extend GCP Residency Detection Support --- src/gcp-residency.ts | 103 +++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 39 +++++++++++----- 2 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 src/gcp-residency.ts diff --git a/src/gcp-residency.ts b/src/gcp-residency.ts new file mode 100644 index 0000000..a7e7243 --- /dev/null +++ b/src/gcp-residency.ts @@ -0,0 +1,103 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {execSync} from 'child_process'; +import {readFileSync} from 'fs'; + +/** + * Known paths unique to Google Compute Engine Linux instances + */ +export const GCE_LINUX_BIOS_PATHS = { + BIOS_DATE: '/sys/class/dmi/id/bios_date', + BIOS_VENDOR: '/sys/class/dmi/id/bios_vendor', +}; + +/** + * Determines if the process is running on a Cloud Functions instance. + * + * Uses the {@link https://cloud.google.com/functions/docs/env-var Cloud Functions environment variables}. + * + * @returns {boolean} `true` if the process is running on Cloud Functions, `false` otherwise. + */ +export function isGoogleCloudFunction(): boolean { + /** + * `K_SERVICE` and `FUNCTION_NAME` are variables unique to Cloud Functions environments: + * - `FUNCTION_NAME` in {@link https://cloud.google.com/functions/docs/env-var Python 3.7 and Go 1.11}. + * - `K_SERVICE` in {@link https://cloud.google.com/functions/docs/env-var Newer runtimes}. + */ + const isGFEnvironment = process.env.K_SERVICE || process.env.FUNCTION_NAME; + + return !!isGFEnvironment; +} + +/** + * Determines if the process is running on a Linux Google Compute Engine instance. + * + * @returns {boolean} `true` if the process is running on Linux Google Compute Engine, `false` otherwise. + */ +export function isGoogleComputeEngineLinux(): boolean { + if (process.platform !== 'linux') return false; + + try { + // ensure this file exist + readFileSync(GCE_LINUX_BIOS_PATHS.BIOS_DATE, 'utf8'); + + // ensure this file exist and matches + const biosVendor = readFileSync(GCE_LINUX_BIOS_PATHS.BIOS_VENDOR, 'utf8'); + + return /Google/.test(biosVendor); + } catch { + return false; + } +} + +/** + * Determines if the process is running on a Windows Google Compute Engine instance. + * + * @returns {boolean} `true` if the process is running on Windows GCE, `false` otherwise. + */ +export function isGoogleComputeEngineWindows(): boolean { + if (process.platform !== 'win32') return false; + + try { + // Retrieve BIOS DMI information using WMI under Microsoft PowerShell + const q = + 'Get-WMIObject -Query "SELECT ReleaseDate, Manufacturer FROM Win32_BIOS"'; + const results = execSync(q, {shell: 'powershell.exe'}).toString(); + + return /Manufacturer\s*:\s*Google/.test(results); + } catch (e) { + return false; + } +} + +/** + * Determines if the process is running on a Google Compute Engine instance. + * + * @returns {boolean} `true` if the process is running on GCE, `false` otherwise. + */ +export function isGoogleComputeEngine(): boolean { + return isGoogleComputeEngineLinux() || isGoogleComputeEngineWindows(); +} + +/** + * Determines if the process is running on Google Cloud Platform. + * + * @returns {boolean} `true` if the process is running on GCP, `false` otherwise. + */ +export function detectGCPResidency(): boolean { + return isGoogleCloudFunction() || isGoogleComputeEngine(); +} diff --git a/src/index.ts b/src/index.ts index aed6b85..01ca689 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import {GaxiosError, GaxiosOptions, GaxiosResponse, request} from 'gaxios'; import {OutgoingHttpHeaders} from 'http'; import jsonBigint = require('json-bigint'); +import {detectGCPResidency} from './gcp-residency'; export const BASE_PATH = '/computeMetadata/v1'; export const HOST_ADDRESS = 'http://169.254.169.254'; @@ -265,19 +266,35 @@ export function resetIsAvailableCache() { cachedIsAvailableResponse = undefined; } +/** + * A cache for the detected GCP Residency. + */ +export let gcpResidencyCache: boolean | null = null; + +/** + * Sets the detected GCP Residency. + * Useful for forcing metadata server detection behavior. + * + * Set `null` to autodetect the environment (default behavior). + */ +export function setGCPResidency(value: boolean | null = null) { + gcpResidencyCache = value !== null ? value : detectGCPResidency(); +} + /** * Obtain the timeout for requests to the metadata server. + * + * In certain environments and conditions requests can take longer than + * the default timeout to complete. This function will determine the + * appropriate timeout based on the environment. + * + * @returns {number} a request timeout duration in milliseconds. */ export function requestTimeout(): number { - // In testing, we were able to reproduce behavior similar to - // https://github.com/googleapis/google-auth-library-nodejs/issues/798 - // by making many concurrent network requests. Requests do not actually fail, - // rather they take significantly longer to complete (and we hit our - // default 3000ms timeout). - // - // This logic detects a GCF environment, using the documented environment - // variables K_SERVICE and FUNCTION_NAME: - // https://cloud.google.com/functions/docs/env-var and, in a GCF environment - // eliminates timeouts (by setting the value to 0 to disable). - return process.env.K_SERVICE || process.env.FUNCTION_NAME ? 0 : 3000; + // Detecting the residency can be resource-intensive. Let's cache the result. + if (gcpResidencyCache === null) { + gcpResidencyCache = detectGCPResidency(); + } + + return gcpResidencyCache ? 0 : 3000; } From b109648d3115b251a9e7525da8f3e2d709fe0847 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 16 Aug 2022 16:24:28 -0700 Subject: [PATCH 2/9] test: update tests --- test/index.test.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index cb41ebc..2828149 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -26,7 +26,7 @@ const HEADERS = { nock.disableNetConnect(); process.removeAllListeners('warning'); -describe('system test', () => { +describe('unit test', () => { const originalGceMetadataIp = process.env.GCE_METADATA_HOST; beforeEach(() => { @@ -35,6 +35,7 @@ describe('system test', () => { delete process.env.GCE_METADATA_HOST; delete process.env.GCE_METADATA_IP; gcp.resetIsAvailableCache(); + gcp.setGCPResidency(); }); afterEach(() => { @@ -482,19 +483,15 @@ describe('system test', () => { assert.strictEqual(isGCE, false); }); - it('returns request timeout of 3000ms, when not GCF', () => { - assert.strictEqual(gcp.requestTimeout(), 3000); - }); - - it('returns request timeout of 0, when FUNCTION_NAME set', () => { - process.env.FUNCTION_NAME = 'my-function'; - assert.strictEqual(gcp.requestTimeout(), 0); - delete process.env.FUNCTION_NAME; - }); + describe('requestTimeout', () => { + it('should return a request timeout of `0` when running on GCP', () => { + gcp.setGCPResidency(true); + assert.strictEqual(gcp.requestTimeout(), 0); + }); - it('returns request timeout of 0, when K_SERVICE set', () => { - process.env.K_SERVICE = 'my-function'; - assert.strictEqual(gcp.requestTimeout(), 0); - delete process.env.K_SERVICE; + it('should return a request timeout of `3000` when not running on GCP', () => { + gcp.setGCPResidency(false); + assert.strictEqual(gcp.requestTimeout(), 3000); + }); }); }); From 3ee5472501b9e4915c00c286cdce7fbf25eced7c Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 16 Aug 2022 16:24:58 -0700 Subject: [PATCH 3/9] test: init `gcp-residency` tests --- test/gcp-residency.test.ts | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 test/gcp-residency.test.ts diff --git a/test/gcp-residency.test.ts b/test/gcp-residency.test.ts new file mode 100644 index 0000000..17cb58e --- /dev/null +++ b/test/gcp-residency.test.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {strict as assert} from 'assert'; +import {beforeEach, describe, it} from 'mocha'; + +import * as gcpResidency from '../src/gcp-residency'; + +describe('gcp-residency', () => { + const ENVIRONMENT_BACKUP = {...process.env}; + + beforeEach(() => { + process.env = {...ENVIRONMENT_BACKUP}; + }); + + describe('isGoogleCloudFunction', () => { + // . + }); + + describe('isGoogleComputeEngineLinux', () => { + // . + }); + + describe('isGoogleComputeEngineWindows', () => { + // . + }); + + describe('isGoogleComputeEngine', () => { + // . + }); + + describe('detectGCPResidency', () => { + it('should run', () => { + assert.equal(typeof gcpResidency.detectGCPResidency(), 'boolean'); + }); + }); +}); From 2955e7d28f9a7f09a24b0dab9b8d0d112e8b59bf Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 16 Aug 2022 16:25:24 -0700 Subject: [PATCH 4/9] chore: add temporary debug logs for `windows` --- src/gcp-residency.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gcp-residency.ts b/src/gcp-residency.ts index a7e7243..f4cafca 100644 --- a/src/gcp-residency.ts +++ b/src/gcp-residency.ts @@ -78,8 +78,14 @@ export function isGoogleComputeEngineWindows(): boolean { 'Get-WMIObject -Query "SELECT ReleaseDate, Manufacturer FROM Win32_BIOS"'; const results = execSync(q, {shell: 'powershell.exe'}).toString(); + // TEMP: debug for Windows + console.dir({results}); + return /Manufacturer\s*:\s*Google/.test(results); } catch (e) { + // TEMP: debug for Windows + console.dir({e}); + return false; } } From 7fb6be1b4e6df57da2035154715f3624f7ddadd0 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 16 Aug 2022 16:51:15 -0700 Subject: [PATCH 5/9] chore: Remove debug logs for windows --- src/gcp-residency.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/gcp-residency.ts b/src/gcp-residency.ts index f4cafca..f7bbed7 100644 --- a/src/gcp-residency.ts +++ b/src/gcp-residency.ts @@ -74,18 +74,13 @@ export function isGoogleComputeEngineWindows(): boolean { try { // Retrieve BIOS DMI information using WMI under Microsoft PowerShell - const q = + const query = 'Get-WMIObject -Query "SELECT ReleaseDate, Manufacturer FROM Win32_BIOS"'; - const results = execSync(q, {shell: 'powershell.exe'}).toString(); - - // TEMP: debug for Windows - console.dir({results}); + const results = execSync(query, {shell: 'powershell.exe'}).toString(); + // Matches 'Manufacturer' + optional, varying spacing + ':' + optional, varying spacing + 'Google' return /Manufacturer\s*:\s*Google/.test(results); - } catch (e) { - // TEMP: debug for Windows - console.dir({e}); - + } catch { return false; } } From ec502898b243173d1539f390e2f266b4b1f16f76 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 16 Aug 2022 17:36:46 -0700 Subject: [PATCH 6/9] test: Add some tests for `gcp-residency` --- test/gcp-residency.test.ts | 103 ++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 8 deletions(-) diff --git a/test/gcp-residency.test.ts b/test/gcp-residency.test.ts index 17cb58e..67cc995 100644 --- a/test/gcp-residency.test.ts +++ b/test/gcp-residency.test.ts @@ -15,36 +15,123 @@ */ import {strict as assert} from 'assert'; +import {execSync} from 'child_process'; +import * as fs from 'fs'; + import {beforeEach, describe, it} from 'mocha'; import * as gcpResidency from '../src/gcp-residency'; -describe('gcp-residency', () => { - const ENVIRONMENT_BACKUP = {...process.env}; +function getLinuxBiosVendor() { + if (process.platform !== 'linux') return ''; + + try { + return fs.readFileSync( + gcpResidency.GCE_LINUX_BIOS_PATHS.BIOS_VENDOR, + 'utf8' + ); + } catch { + return ''; + } +} + +function getWindowsBIOSManufacturer() { + if (process.platform !== 'win32') return ''; + + const query = 'Get-WMIObject -Query "SELECT Manufacturer FROM Win32_BIOS"'; + return execSync(query, {shell: 'powershell.exe'}).toString(); +} + +const ENVIRONMENT_BACKUP = {...process.env}; +const EXPECT_CLOUD_FUNCTION = !!process.env.K_SERVICE; +const EXPECT_LINUX_GCE = /Google/.test(getLinuxBiosVendor()); +const EXPECT_WINDOWS_GCE = /Google/.test(getWindowsBIOSManufacturer()); +/** A simple block to help with debugging failed tests. */ +const DEBUG_HELP = JSON.stringify({ + processEnv: process.env, + getLinuxBiosVendor: getLinuxBiosVendor(), + getWindowsBIOSManufacturer: getWindowsBIOSManufacturer(), +}); + +describe('gcp-residency', () => { beforeEach(() => { process.env = {...ENVIRONMENT_BACKUP}; }); describe('isGoogleCloudFunction', () => { - // . + it('should return `true` if `K_SERVICE` env is set', () => { + process.env.K_SERVICE = '1'; + delete process.env.FUNCTION_NAME; + + assert(gcpResidency.isGoogleCloudFunction()); + }); + + it('should return `true` if `FUNCTION_NAME` env is set', () => { + process.env.FUNCTION_NAME = '1'; + delete process.env.K_SERVICE; + + assert(gcpResidency.isGoogleCloudFunction()); + }); + + it('should return `false` if neither `K_SERVICE` nor `FUNCTION_NAME` are set', () => { + delete process.env.FUNCTION_NAME; + delete process.env.K_SERVICE; + + assert.equal(gcpResidency.isGoogleCloudFunction(), false); + }); + + it('should return the expected result', () => { + assert.equal( + gcpResidency.isGoogleCloudFunction(), + EXPECT_CLOUD_FUNCTION, + `Expecting \`isGoogleCloudFunction()\` to be \`${EXPECT_CLOUD_FUNCTION}\`. Details: ${DEBUG_HELP}` + ); + }); }); describe('isGoogleComputeEngineLinux', () => { - // . + it('should return the expected result', () => { + assert.equal( + gcpResidency.isGoogleComputeEngineLinux(), + EXPECT_LINUX_GCE, + `Expecting \`isGoogleComputeEngineLinux()\` to be \`${EXPECT_LINUX_GCE}\`. Details: ${DEBUG_HELP}` + ); + }); }); describe('isGoogleComputeEngineWindows', () => { - // . + it('should return the expected result', () => { + assert.equal( + gcpResidency.isGoogleComputeEngineWindows(), + EXPECT_WINDOWS_GCE, + `Expecting \`isGoogleComputeEngineWindows()\` to be \`${EXPECT_WINDOWS_GCE}\`. Details: ${DEBUG_HELP}` + ); + }); }); describe('isGoogleComputeEngine', () => { - // . + it('should return the expected result', () => { + const onGCE = EXPECT_WINDOWS_GCE || EXPECT_LINUX_GCE; + + assert.equal( + gcpResidency.isGoogleComputeEngine(), + onGCE, + `Expecting \`isGoogleComputeEngineWindows()\` to be \`${onGCE}\`. Details: ${DEBUG_HELP}` + ); + }); }); describe('detectGCPResidency', () => { - it('should run', () => { - assert.equal(typeof gcpResidency.detectGCPResidency(), 'boolean'); + it('should return the expected result', () => { + const onGCP = + EXPECT_CLOUD_FUNCTION || EXPECT_LINUX_GCE || EXPECT_WINDOWS_GCE; + + assert.equal( + gcpResidency.detectGCPResidency(), + onGCP, + `Expecting \`isGoogleComputeEngineWindows()\` to be \`${onGCP}\`. Details: ${DEBUG_HELP}` + ); }); }); }); From 87ebbc87d3b08ea9387c787cd222373164f5b5fc Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 1 Sep 2022 12:40:42 -0700 Subject: [PATCH 7/9] refactor: use MAC Address to determine GCE residency --- package.json | 2 + src/gcp-residency.ts | 65 ++++-------------- test/gcp-residency.test.ts | 133 ++++++++++++++++--------------------- 3 files changed, 75 insertions(+), 125 deletions(-) diff --git a/package.json b/package.json index 5772717..7937077 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/mocha": "^9.0.0", "@types/ncp": "^2.0.1", "@types/node": "^17.0.25", + "@types/sinon": "^10.0.13", "@types/tmp": "0.2.3", "@types/uuid": "^8.0.0", "c8": "^7.0.0", @@ -58,6 +59,7 @@ "mocha": "^8.0.0", "ncp": "^2.0.0", "nock": "^13.0.0", + "sinon": "^14.0.0", "tmp": "^0.2.0", "typescript": "^4.6.3", "uuid": "^8.0.0" diff --git a/src/gcp-residency.ts b/src/gcp-residency.ts index f7bbed7..f79355e 100644 --- a/src/gcp-residency.ts +++ b/src/gcp-residency.ts @@ -14,16 +14,9 @@ * limitations under the License. */ -import {execSync} from 'child_process'; -import {readFileSync} from 'fs'; +import {networkInterfaces} from 'os'; -/** - * Known paths unique to Google Compute Engine Linux instances - */ -export const GCE_LINUX_BIOS_PATHS = { - BIOS_DATE: '/sys/class/dmi/id/bios_date', - BIOS_VENDOR: '/sys/class/dmi/id/bios_vendor', -}; +const GCE_MAC_ADDRESS_REGEX = /^42:01/; /** * Determines if the process is running on a Cloud Functions instance. @@ -44,54 +37,24 @@ export function isGoogleCloudFunction(): boolean { } /** - * Determines if the process is running on a Linux Google Compute Engine instance. - * - * @returns {boolean} `true` if the process is running on Linux Google Compute Engine, `false` otherwise. - */ -export function isGoogleComputeEngineLinux(): boolean { - if (process.platform !== 'linux') return false; - - try { - // ensure this file exist - readFileSync(GCE_LINUX_BIOS_PATHS.BIOS_DATE, 'utf8'); - - // ensure this file exist and matches - const biosVendor = readFileSync(GCE_LINUX_BIOS_PATHS.BIOS_VENDOR, 'utf8'); - - return /Google/.test(biosVendor); - } catch { - return false; - } -} - -/** - * Determines if the process is running on a Windows Google Compute Engine instance. + * Determines if the process is running on a Google Compute Engine instance. * - * @returns {boolean} `true` if the process is running on Windows GCE, `false` otherwise. + * @returns {boolean} `true` if the process is running on GCE, `false` otherwise. */ -export function isGoogleComputeEngineWindows(): boolean { - if (process.platform !== 'win32') return false; +export function isGoogleComputeEngine(): boolean { + const interfaces = networkInterfaces(); - try { - // Retrieve BIOS DMI information using WMI under Microsoft PowerShell - const query = - 'Get-WMIObject -Query "SELECT ReleaseDate, Manufacturer FROM Win32_BIOS"'; - const results = execSync(query, {shell: 'powershell.exe'}).toString(); + for (const item of Object.values(interfaces)) { + if (!item) continue; - // Matches 'Manufacturer' + optional, varying spacing + ':' + optional, varying spacing + 'Google' - return /Manufacturer\s*:\s*Google/.test(results); - } catch { - return false; + for (const {mac} of item) { + if (GCE_MAC_ADDRESS_REGEX.test(mac)) { + return true; + } + } } -} -/** - * Determines if the process is running on a Google Compute Engine instance. - * - * @returns {boolean} `true` if the process is running on GCE, `false` otherwise. - */ -export function isGoogleComputeEngine(): boolean { - return isGoogleComputeEngineLinux() || isGoogleComputeEngineWindows(); + return false; } /** diff --git a/test/gcp-residency.test.ts b/test/gcp-residency.test.ts index 67cc995..678df4e 100644 --- a/test/gcp-residency.test.ts +++ b/test/gcp-residency.test.ts @@ -15,50 +15,40 @@ */ import {strict as assert} from 'assert'; -import {execSync} from 'child_process'; -import * as fs from 'fs'; +import * as os from 'os'; import {beforeEach, describe, it} from 'mocha'; +import {SinonSandbox, createSandbox} from 'sinon'; import * as gcpResidency from '../src/gcp-residency'; -function getLinuxBiosVendor() { - if (process.platform !== 'linux') return ''; - - try { - return fs.readFileSync( - gcpResidency.GCE_LINUX_BIOS_PATHS.BIOS_VENDOR, - 'utf8' - ); - } catch { - return ''; - } -} - -function getWindowsBIOSManufacturer() { - if (process.platform !== 'win32') return ''; - - const query = 'Get-WMIObject -Query "SELECT Manufacturer FROM Win32_BIOS"'; - return execSync(query, {shell: 'powershell.exe'}).toString(); -} - const ENVIRONMENT_BACKUP = {...process.env}; -const EXPECT_CLOUD_FUNCTION = !!process.env.K_SERVICE; -const EXPECT_LINUX_GCE = /Google/.test(getLinuxBiosVendor()); -const EXPECT_WINDOWS_GCE = /Google/.test(getWindowsBIOSManufacturer()); - -/** A simple block to help with debugging failed tests. */ -const DEBUG_HELP = JSON.stringify({ - processEnv: process.env, - getLinuxBiosVendor: getLinuxBiosVendor(), - getWindowsBIOSManufacturer: getWindowsBIOSManufacturer(), -}); describe('gcp-residency', () => { + let sandbox: SinonSandbox; + beforeEach(() => { process.env = {...ENVIRONMENT_BACKUP}; + sandbox = createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); }); + /** + * A simple utility for stubbing the networkInterface for GCE emulation. + * + * @param isGCE determines if the address should begin with `42:01` or not + */ + function setGCENetworkInterface(isGCE = true) { + const mac = isGCE ? '42:01:00:00:00:00' : '00:00:00:00:00:00'; + + sandbox.stub(os, 'networkInterfaces').returns({ + 'test-interface': [{mac} as os.NetworkInterfaceInfo], + }); + } + describe('isGoogleCloudFunction', () => { it('should return `true` if `K_SERVICE` env is set', () => { process.env.K_SERVICE = '1'; @@ -80,58 +70,53 @@ describe('gcp-residency', () => { assert.equal(gcpResidency.isGoogleCloudFunction(), false); }); + }); + + describe('isGoogleComputeEngine', () => { + it('should return `true` if the host MAC address begins with `42:01`', () => { + setGCENetworkInterface(true); - it('should return the expected result', () => { - assert.equal( - gcpResidency.isGoogleCloudFunction(), - EXPECT_CLOUD_FUNCTION, - `Expecting \`isGoogleCloudFunction()\` to be \`${EXPECT_CLOUD_FUNCTION}\`. Details: ${DEBUG_HELP}` - ); + assert.equal(gcpResidency.isGoogleComputeEngine(), true); }); - }); - describe('isGoogleComputeEngineLinux', () => { - it('should return the expected result', () => { - assert.equal( - gcpResidency.isGoogleComputeEngineLinux(), - EXPECT_LINUX_GCE, - `Expecting \`isGoogleComputeEngineLinux()\` to be \`${EXPECT_LINUX_GCE}\`. Details: ${DEBUG_HELP}` - ); + it('should return `false` if the host MAC address does not begin with `42:01`', () => { + setGCENetworkInterface(false); + + assert.equal(gcpResidency.isGoogleComputeEngine(), false); }); }); - describe('isGoogleComputeEngineWindows', () => { - it('should return the expected result', () => { - assert.equal( - gcpResidency.isGoogleComputeEngineWindows(), - EXPECT_WINDOWS_GCE, - `Expecting \`isGoogleComputeEngineWindows()\` to be \`${EXPECT_WINDOWS_GCE}\`. Details: ${DEBUG_HELP}` - ); + describe('detectGCPResidency', () => { + it('should return `true` if `isGoogleCloudFunction`', () => { + // `isGoogleCloudFunction` = true + process.env.K_SERVICE = '1'; + + // `isGoogleComputeEngine` = false + setGCENetworkInterface(false); + + assert(gcpResidency.detectGCPResidency()); }); - }); - describe('isGoogleComputeEngine', () => { - it('should return the expected result', () => { - const onGCE = EXPECT_WINDOWS_GCE || EXPECT_LINUX_GCE; - - assert.equal( - gcpResidency.isGoogleComputeEngine(), - onGCE, - `Expecting \`isGoogleComputeEngineWindows()\` to be \`${onGCE}\`. Details: ${DEBUG_HELP}` - ); + it('should return `true` if `isGoogleComputeEngine`', () => { + // `isGoogleCloudFunction` = false + delete process.env.FUNCTION_NAME; + delete process.env.K_SERVICE; + + // `isGoogleComputeEngine` = true + setGCENetworkInterface(true); + + assert(gcpResidency.detectGCPResidency()); }); - }); - describe('detectGCPResidency', () => { - it('should return the expected result', () => { - const onGCP = - EXPECT_CLOUD_FUNCTION || EXPECT_LINUX_GCE || EXPECT_WINDOWS_GCE; - - assert.equal( - gcpResidency.detectGCPResidency(), - onGCP, - `Expecting \`isGoogleComputeEngineWindows()\` to be \`${onGCP}\`. Details: ${DEBUG_HELP}` - ); + it('should return `false` !`isGoogleCloudFunction` && !`isGoogleComputeEngine`', () => { + // `isGoogleCloudFunction` = false + delete process.env.FUNCTION_NAME; + delete process.env.K_SERVICE; + + // `isGoogleComputeEngine` = false + setGCENetworkInterface(false); + + assert.equal(gcpResidency.detectGCPResidency(), false); }); }); }); From 5e22ef3dc51c9fe5b362e61b4f5cf42b26826ca5 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 10 Oct 2022 13:09:35 -0700 Subject: [PATCH 8/9] refactor: Re-Add Linux GCE detection --- src/gcp-residency.ts | 48 +++++++++++++++++++++++--- test/gcp-residency.test.ts | 69 +++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/gcp-residency.ts b/src/gcp-residency.ts index f79355e..8ca9505 100644 --- a/src/gcp-residency.ts +++ b/src/gcp-residency.ts @@ -14,7 +14,16 @@ * limitations under the License. */ -import {networkInterfaces} from 'os'; +import {readFileSync, statSync} from 'fs'; +import {networkInterfaces, platform} from 'os'; + +/** + * Known paths unique to Google Compute Engine Linux instances + */ +export const GCE_LINUX_BIOS_PATHS = { + BIOS_DATE: '/sys/class/dmi/id/bios_date', + BIOS_VENDOR: '/sys/class/dmi/id/bios_vendor', +}; const GCE_MAC_ADDRESS_REGEX = /^42:01/; @@ -37,11 +46,33 @@ export function isGoogleCloudFunction(): boolean { } /** - * Determines if the process is running on a Google Compute Engine instance. + * Determines if the process is running on a Linux Google Compute Engine instance. * - * @returns {boolean} `true` if the process is running on GCE, `false` otherwise. + * @returns {boolean} `true` if the process is running on Linux GCE, `false` otherwise. */ -export function isGoogleComputeEngine(): boolean { +export function isGoogleComputeEngineLinux(): boolean { + if (platform() !== 'linux') return false; + + try { + // ensure this file exist + statSync(GCE_LINUX_BIOS_PATHS.BIOS_DATE); + + // ensure this file exist and matches + const biosVendor = readFileSync(GCE_LINUX_BIOS_PATHS.BIOS_VENDOR, 'utf8'); + + return /Google/.test(biosVendor); + } catch { + return false; + } +} + +/** + * Determines if the process is running on a Google Compute Engine instance with a known + * MAC address. + * + * @returns {boolean} `true` if the process is running on GCE (as determined by MAC address), `false` otherwise. + */ +export function isGoogleComputeEngineMACAddress(): boolean { const interfaces = networkInterfaces(); for (const item of Object.values(interfaces)) { @@ -57,6 +88,15 @@ export function isGoogleComputeEngine(): boolean { return false; } +/** + * Determines if the process is running on a Google Compute Engine instance. + * + * @returns {boolean} `true` if the process is running on GCE, `false` otherwise. + */ +export function isGoogleComputeEngine(): boolean { + return isGoogleComputeEngineLinux() || isGoogleComputeEngineMACAddress(); +} + /** * Determines if the process is running on Google Cloud Platform. * diff --git a/test/gcp-residency.test.ts b/test/gcp-residency.test.ts index 678df4e..93b283b 100644 --- a/test/gcp-residency.test.ts +++ b/test/gcp-residency.test.ts @@ -15,6 +15,7 @@ */ import {strict as assert} from 'assert'; +import * as fs from 'fs'; import * as os from 'os'; import {beforeEach, describe, it} from 'mocha'; @@ -49,6 +50,44 @@ describe('gcp-residency', () => { }); } + /** + * A simple utility for stubbing the platform for GCE emulation. + * + * @param platform a Node.js platform + */ + function setGCEPlatform(platform: NodeJS.Platform = 'linux') { + sandbox.stub(os, 'platform').returns(platform); + } + + /** + * A simple utility for stubbing the Linux BIOS files for GCE emulation. + * + * @param isGCE options: + * - set `true` to simulate the files exist and are GCE + * - set `false` for exist, but are not GCE + * - set `null` for simulate ENOENT + */ + function setGCELinuxBios(isGCE: boolean | null) { + sandbox.stub(fs, 'statSync').callsFake(path => { + assert.equal(path, gcpResidency.GCE_LINUX_BIOS_PATHS.BIOS_DATE); + + return undefined; + }); + + sandbox.stub(fs, 'readFileSync').callsFake((path, encoding) => { + assert.equal(path, gcpResidency.GCE_LINUX_BIOS_PATHS.BIOS_VENDOR); + assert.equal(encoding, 'utf8'); + + if (isGCE === true) { + return 'x Google x'; + } else if (isGCE === false) { + return 'Sandwich Co.'; + } else { + throw new Error("File doesn't exist"); + } + }); + } + describe('isGoogleCloudFunction', () => { it('should return `true` if `K_SERVICE` env is set', () => { process.env.K_SERVICE = '1'; @@ -73,14 +112,42 @@ describe('gcp-residency', () => { }); describe('isGoogleComputeEngine', () => { + it('should return `true` if on Linux and has the expected BIOS files', () => { + setGCENetworkInterface(false); + setGCEPlatform('linux'); + setGCELinuxBios(true); + + assert.equal(gcpResidency.isGoogleComputeEngine(), true); + }); + + it('should return `false` if on Linux and the expected BIOS files are not GCE', () => { + setGCENetworkInterface(false); + setGCEPlatform('linux'); + setGCELinuxBios(false); + + assert.equal(gcpResidency.isGoogleComputeEngine(), false); + }); + + it('should return `false` if on Linux and the BIOS files do not exist', () => { + setGCENetworkInterface(false); + setGCEPlatform('linux'); + setGCELinuxBios(null); + + assert.equal(gcpResidency.isGoogleComputeEngine(), false); + }); + it('should return `true` if the host MAC address begins with `42:01`', () => { setGCENetworkInterface(true); + setGCEPlatform('win32'); + setGCELinuxBios(null); assert.equal(gcpResidency.isGoogleComputeEngine(), true); }); - it('should return `false` if the host MAC address does not begin with `42:01`', () => { + it('should return `false` if the host MAC address does not begin with `42:01` & is not Linux', () => { setGCENetworkInterface(false); + setGCEPlatform('win32'); + setGCELinuxBios(null); assert.equal(gcpResidency.isGoogleComputeEngine(), false); }); From 7bdf868e005da2e4759c9b7d2b816394b109096d Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Tue, 8 Nov 2022 21:05:25 -0800 Subject: [PATCH 9/9] feat: Extend GCP Serverless Runtime Support - A refactor + Cloud Run Job support --- src/gcp-residency.ts | 29 ++++++++++++++++-------- test/gcp-residency.test.ts | 46 +++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/gcp-residency.ts b/src/gcp-residency.ts index 8ca9505..8c65d26 100644 --- a/src/gcp-residency.ts +++ b/src/gcp-residency.ts @@ -28,19 +28,30 @@ export const GCE_LINUX_BIOS_PATHS = { const GCE_MAC_ADDRESS_REGEX = /^42:01/; /** - * Determines if the process is running on a Cloud Functions instance. + * Determines if the process is running on a Google Cloud Serverless environment (Cloud Run or Cloud Functions instance). * - * Uses the {@link https://cloud.google.com/functions/docs/env-var Cloud Functions environment variables}. + * Uses the: + * - {@link https://cloud.google.com/run/docs/container-contract#env-vars Cloud Run environment variables}. + * - {@link https://cloud.google.com/functions/docs/env-var Cloud Functions environment variables}. * - * @returns {boolean} `true` if the process is running on Cloud Functions, `false` otherwise. + * @returns {boolean} `true` if the process is running on GCP serverless, `false` otherwise. */ -export function isGoogleCloudFunction(): boolean { +export function isGoogleCloudServerless(): boolean { /** - * `K_SERVICE` and `FUNCTION_NAME` are variables unique to Cloud Functions environments: - * - `FUNCTION_NAME` in {@link https://cloud.google.com/functions/docs/env-var Python 3.7 and Go 1.11}. - * - `K_SERVICE` in {@link https://cloud.google.com/functions/docs/env-var Newer runtimes}. + * `CLOUD_RUN_JOB` is used for Cloud Run Jobs + * - See {@link https://cloud.google.com/run/docs/container-contract#env-vars Cloud Run environment variables}. + * + * `FUNCTION_NAME` is used in older Cloud Functions environments: + * - See {@link https://cloud.google.com/functions/docs/env-var Python 3.7 and Go 1.11}. + * + * `K_SERVICE` is used in Cloud Run and newer Cloud Functions environments: + * - See {@link https://cloud.google.com/run/docs/container-contract#env-vars Cloud Run environment variables}. + * - See {@link https://cloud.google.com/functions/docs/env-var Cloud Functions newer runtimes}. */ - const isGFEnvironment = process.env.K_SERVICE || process.env.FUNCTION_NAME; + const isGFEnvironment = + process.env.CLOUD_RUN_JOB || + process.env.FUNCTION_NAME || + process.env.K_SERVICE; return !!isGFEnvironment; } @@ -103,5 +114,5 @@ export function isGoogleComputeEngine(): boolean { * @returns {boolean} `true` if the process is running on GCP, `false` otherwise. */ export function detectGCPResidency(): boolean { - return isGoogleCloudFunction() || isGoogleComputeEngine(); + return isGoogleCloudServerless() || isGoogleComputeEngine(); } diff --git a/test/gcp-residency.test.ts b/test/gcp-residency.test.ts index 93b283b..e50ac01 100644 --- a/test/gcp-residency.test.ts +++ b/test/gcp-residency.test.ts @@ -31,6 +31,7 @@ describe('gcp-residency', () => { beforeEach(() => { process.env = {...ENVIRONMENT_BACKUP}; sandbox = createSandbox(); + removeServerlessEnvironmentVariables(); }); afterEach(() => { @@ -88,26 +89,33 @@ describe('gcp-residency', () => { }); } - describe('isGoogleCloudFunction', () => { - it('should return `true` if `K_SERVICE` env is set', () => { - process.env.K_SERVICE = '1'; - delete process.env.FUNCTION_NAME; + function removeServerlessEnvironmentVariables() { + delete process.env.CLOUD_RUN_JOB; + delete process.env.FUNCTION_NAME; + delete process.env.K_SERVICE; + } + + describe('isGoogleCloudServerless', () => { + it('should return `true` if `CLOUD_RUN_JOB` env is set', () => { + process.env.CLOUD_RUN_JOB = '1'; - assert(gcpResidency.isGoogleCloudFunction()); + assert(gcpResidency.isGoogleCloudServerless()); }); it('should return `true` if `FUNCTION_NAME` env is set', () => { process.env.FUNCTION_NAME = '1'; - delete process.env.K_SERVICE; - assert(gcpResidency.isGoogleCloudFunction()); + assert(gcpResidency.isGoogleCloudServerless()); }); - it('should return `false` if neither `K_SERVICE` nor `FUNCTION_NAME` are set', () => { - delete process.env.FUNCTION_NAME; - delete process.env.K_SERVICE; + it('should return `true` if `K_SERVICE` env is set', () => { + process.env.K_SERVICE = '1'; + + assert(gcpResidency.isGoogleCloudServerless()); + }); - assert.equal(gcpResidency.isGoogleCloudFunction(), false); + it('should return `false` if none of the envs are set', () => { + assert.equal(gcpResidency.isGoogleCloudServerless(), false); }); }); @@ -154,8 +162,8 @@ describe('gcp-residency', () => { }); describe('detectGCPResidency', () => { - it('should return `true` if `isGoogleCloudFunction`', () => { - // `isGoogleCloudFunction` = true + it('should return `true` if `isGoogleCloudServerless`', () => { + // `isGoogleCloudServerless` = true process.env.K_SERVICE = '1'; // `isGoogleComputeEngine` = false @@ -165,9 +173,8 @@ describe('gcp-residency', () => { }); it('should return `true` if `isGoogleComputeEngine`', () => { - // `isGoogleCloudFunction` = false - delete process.env.FUNCTION_NAME; - delete process.env.K_SERVICE; + // `isGoogleCloudServerless` = false + removeServerlessEnvironmentVariables(); // `isGoogleComputeEngine` = true setGCENetworkInterface(true); @@ -175,10 +182,9 @@ describe('gcp-residency', () => { assert(gcpResidency.detectGCPResidency()); }); - it('should return `false` !`isGoogleCloudFunction` && !`isGoogleComputeEngine`', () => { - // `isGoogleCloudFunction` = false - delete process.env.FUNCTION_NAME; - delete process.env.K_SERVICE; + it('should return `false` !`isGoogleCloudServerless` && !`isGoogleComputeEngine`', () => { + // `isGoogleCloudServerless` = false + removeServerlessEnvironmentVariables(); // `isGoogleComputeEngine` = false setGCENetworkInterface(false);