From 9d42821587267441d5d007b7c412495ef3dbf6df Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 10 Nov 2016 18:04:13 -0800 Subject: [PATCH] Adds Project ID Detection in this order: - Project ID from Application Default Credentials - Project ID from GCLOUD_CONFIG environment variable - Project ID from App Engine AppIdentity service - Project ID from Compute Engine metadata server - Project ID from gcloud default config file --- src/AppEngine/AppIdentity.php | 59 +++++ src/ClientTrait.php | 58 +++-- src/DetectProjectTrait.php | 182 ++++++++++++++++ tests/unit/ClientTraitTest.php | 104 ++++++++- tests/unit/DetectProjectTrait.php | 241 +++++++++++++++++++++ tests/unit/fixtures/config_default_fixture | 2 + 6 files changed, 610 insertions(+), 36 deletions(-) create mode 100644 src/AppEngine/AppIdentity.php create mode 100644 src/DetectProjectTrait.php create mode 100644 tests/unit/DetectProjectTrait.php create mode 100644 tests/unit/fixtures/config_default_fixture diff --git a/src/AppEngine/AppIdentity.php b/src/AppEngine/AppIdentity.php new file mode 100644 index 000000000000..69c340c19a3c --- /dev/null +++ b/src/AppEngine/AppIdentity.php @@ -0,0 +1,59 @@ +getApplicationId(); + * ``` + */ +class AppIdentity +{ + /** + * Get the current application ID from the App Identity Service + * + * Example: + * ``` + * $projectId = $appIdentity->getApplicationId(); + * ``` + * + * @return string + */ + public function getApplicationId() + { + if (!class_exists('google\appengine\api\app_identity\AppIdentityService')) { + throw new \Exception('This class must be run in App Engine'); + } + + return AppIdentityService::getApplicationId(); + } +} diff --git a/src/ClientTrait.php b/src/ClientTrait.php index e2f9e63b5256..ac7c0ba488a6 100644 --- a/src/ClientTrait.php +++ b/src/ClientTrait.php @@ -30,6 +30,8 @@ */ trait ClientTrait { + use DetectProjectTrait; + /** * @var string The project ID created in the Google Developers Console. */ @@ -134,9 +136,14 @@ private function getKeyFile(array $config = []) * 1. If $config['projectId'] is set, use that. * 2. If $config['keyFile'] is set, attempt to retrieve a project ID from * that. - * 3. If code is running on compute engine, try to get the project ID from + * 3. If the GCLOUD_PROJECT environment variable is set, return this value + * 4. If code is running on App Engine, try to get the project ID from the + * App Identity service + * 5. If code is running on compute engine, try to get the project ID from * the metadata store - * 4. Throw exception. + * 6. If the OS-specific gcloud configuration file is set, load it and get + * the project ID from that. + * 7. Throw exception. * * @param array $config * @return string @@ -157,40 +164,27 @@ private function detectProjectId(array $config) return $config['keyFile']['project_id']; } - if ($this->onGce($config['httpHandler'])) { - $metadata = $this->getMetaData(); - $projectId = $metadata->getProjectId(); - if ($projectId) { - return $projectId; - } + $projectId = $this->projectFromEnvVar(); + if (!$projectId) { + $projectId = $this->projectFromAppEngine(); } - throw new GoogleException( - 'No project ID was provided, ' . - 'and we were unable to detect a default project ID.' - ); - } + if (!$projectId) { + $projectId = $this->projectFromGce(); + } - /** - * Abstract the GCECredentials call so we can mock it in the unit tests! - * - * @codeCoverageIgnore - * @return bool - */ - protected function onGce($httpHandler) - { - return GCECredentials::onGce($httpHandler); - } + if (!$projectId) { + $projectId = $this->projectFromGcloudConfig(); + } - /** - * Abstract the Metadata instantiation for unit testing - * - * @codeCoverageIgnore - * @return Metadata - */ - protected function getMetaData() - { - return new Metadata; + if (!$projectId) { + throw new GoogleException( + 'No project ID was provided, ' . + 'and we were unable to detect a default project ID.' + ); + } + + return $projectId; } /** diff --git a/src/DetectProjectTrait.php b/src/DetectProjectTrait.php new file mode 100644 index 000000000000..a8d5ffa61367 --- /dev/null +++ b/src/DetectProjectTrait.php @@ -0,0 +1,182 @@ +getEnvVar()); + } + + /** + * Return a project ID from App Engine. + * + * @return string|null $projectId + */ + private function projectFromAppEngine() + { + if ($this->onAppEngine()) { + $appIdentity = $this->getAppIdentity(); + $projectId = $appIdentity->getApplicationId(); + if ($projectId) { + return $projectId; + } + } + } + + /** + * Return a project ID from GCE. + * + * @return string|null $projectId + */ + private function projectFromGce($httpHandler = null) + { + if ($this->onGce($httpHandler)) { + $metadata = $this->getMetaData(); + $projectId = $metadata->getProjectId(); + if ($projectId) { + return $projectId; + } + } + } + + /** + * The gcloud config path is OS dependent: + * - windows: %APPDATA%/gcloud/configurations/config_default + * - others: $HOME/.config/gcloud/configurations/config_default + * + * If the file does not exists, this returns null. + * + * @return string|null $projectId + */ + private function projectFromGcloudConfig() + { + $path = $this->pathToGcloudConfig(); + if (!file_exists($path)) { + return; + } + $configDefault = parse_ini_file($path, true); + if (isset($configDefault['core']['project'])) { + return $configDefault['core']['project']; + } + } + + /** + * Determine the path to the gcloud configuration path + * + * @codeCoverageIgnore + * @return bool + */ + protected function pathToGcloudConfig() + { + if ($this->onWindows()) { + $path = [getenv('APPDATA')]; + } else { + $path = [ + getenv('HOME'), + CredentialsLoader::NON_WINDOWS_WELL_KNOWN_PATH_BASE + ]; + } + $path[] = 'gcloud/configurations/config_default'; + return implode(DIRECTORY_SEPARATOR, $path); + } + + /** + * Abstract the CredentialsLoader call so we can mock it in the unit tests! + * + * @codeCoverageIgnore + * @return bool + */ + protected function onWindows() + { + return CredentialsLoader::isOnWindows(); + } + + /** + * Abstract the AppIdentity call so we can mock it in the unit tests! + * + * @codeCoverageIgnore + * @return bool + */ + protected function onAppEngine() + { + return AppIdentityCredentials::onAppEngine() && + !GCECredentials::onAppEngineFlexible(); + } + + /** + * Abstract the AppIdentity instantiation for unit testing + * + * @codeCoverageIgnore + * @return Metadata + */ + protected function getAppIdentity() + { + return new AppIdentity; + } + + /** + * Abstract the GCECredentials call so we can mock it in the unit tests! + * + * @codeCoverageIgnore + * @return bool + */ + protected function onGce($httpHandler) + { + return GCECredentials::onGce($httpHandler); + } + + /** + * Abstract the Metadata instantiation for unit testing + * + * @codeCoverageIgnore + * @return Metadata + */ + protected function getMetaData() + { + return new Metadata; + } + + /** + * Abstract the Environment Variable name for unit testing + * + * @codeCoverageIgnore + * @return string + */ + protected function getEnvVar() + { + return 'GCLOUD_PROJECT'; + } +} diff --git a/tests/unit/ClientTraitTest.php b/tests/unit/ClientTraitTest.php index 4e581f3e9a3e..00319acfc60a 100644 --- a/tests/unit/ClientTraitTest.php +++ b/tests/unit/ClientTraitTest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Tests; +use Google\Cloud\AppEngine\AppIdentity; use Google\Cloud\ClientTrait; use Google\Cloud\Compute\Metadata; use GuzzleHttp\Psr7\Response; @@ -166,6 +167,50 @@ public function testDetectProjectIdOnGce() $this->assertEquals($res, $projectId); } + public function testDetectProjectIdOnAppEngine() + { + $projectId = 'appengine-project-rawks'; + + $m = $this->prophesize(AppIdentity::class); + $m->getApplicationId()->willReturn($projectId)->shouldBeCalled(); + + $trait = new ClientTraitStubOnAppEngine($m); + + $res = $trait->runDetectProjectId([]); + + $this->assertEquals($res, $projectId); + } + + public function testDetectProjectIdWithEnvVar() + { + $projectId = 'appengineflex-project-rawks'; + + // set a test environment variable + putenv('GOOGLE_DETECT_PROJECT_TEST_PROJECT_ID=' . $projectId); + + $trait = new ClientTraitStubWithEnvVar(); + + $res = $trait->runDetectProjectId([]); + + // remove the environment variable + putenv('GOOGLE_DETECT_PROJECT_TEST_PROJECT_ID'); + + $this->assertEquals($res, $projectId); + } + + public function testDetectProjectIdWithGcloudConfig() + { + $projectId = 'gcloud-project-rawks'; + + $configPath = __DIR__ . '/fixtures/config_default_fixture'; + + $trait = new ClientTraitStubWithGcloudConfig($configPath); + + $res = $trait->runDetectProjectId([]); + + $this->assertEquals($res, $projectId); + } + /** * @expectedException Google\Cloud\Exception\GoogleException */ @@ -205,12 +250,50 @@ public function runDetectProjectId($config) { return $this->detectProjectId($config); } + + protected function getEnvVar() + { + // ensure no evar is accidentally used + return ''; + } + + protected function pathToGcloudConfig() + { + // ensure no file is accidentally used + return ''; + } } -class ClientTraitStubOnGce extends ClientTraitStub +class ClientTraitStubWithEnvVar extends ClientTraitStub { - use ClientTrait; + protected function getEnvVar() + { + return 'GOOGLE_DETECT_PROJECT_TEST_PROJECT_ID'; + } +} + +class ClientTraitStubOnAppEngine extends ClientTraitStub +{ + private $appIdentity; + + public function __construct($appIdentity) + { + $this->appIdentity = $appIdentity; + } + + protected function onAppEngine() + { + return true; + } + + protected function getAppIdentity() + { + return $this->appIdentity->reveal(); + } +} +class ClientTraitStubOnGce extends ClientTraitStub +{ private $metadata; public function __construct($metadata) @@ -229,10 +312,23 @@ protected function getMetadata() } } -class ClientTraitStubGrpcDependencyChecks extends ClientTraitStub +class ClientTraitStubWithGcloudConfig extends ClientTraitStub { - use ClientTrait; + private $configPath; + + public function __construct($configPath) + { + $this->configPath = $configPath; + } + + protected function pathToGcloudConfig() + { + return $this->configPath; + } +} +class ClientTraitStubGrpcDependencyChecks extends ClientTraitStub +{ private $dependencyStatus; public function __construct(array $dependencyStatus) diff --git a/tests/unit/DetectProjectTrait.php b/tests/unit/DetectProjectTrait.php new file mode 100644 index 000000000000..201f8432d8b6 --- /dev/null +++ b/tests/unit/DetectProjectTrait.php @@ -0,0 +1,241 @@ +runPathToGcloudConfig(); + + $this->assertEquals($res, $configPath); + } + + public function testPathToGcloudConfigOnWindows() + { + $configPath = 'test/gcloud/configurations/config_default'; + + $trait = new DetectProjectTraitStubOnWindows(); + + putenv('APPDATA=' . 'test'); + + $res = $trait->runPathToGcloudConfig(); + + putenv('APPDATA'); + + $this->assertEquals($res, $configPath); + } + + + public function testDetectProjectIdOnGce() + { + $projectId = 'gce-project-rawks'; + + $m = $this->prophesize(Metadata::class); + $m->getProjectId()->willReturn($projectId)->shouldBeCalled(); + + $trait = new DetectProjectTraitStubOnGce($m); + + $res = $trait->runProjectFromGce([]); + + $this->assertEquals($res, $projectId); + } + + public function testDetectProjectIdOnAppEngine() + { + $projectId = 'appengine-project-rawks'; + + $m = $this->prophesize(AppIdentity::class); + $m->getApplicationId()->willReturn($projectId)->shouldBeCalled(); + + $trait = new DetectProjectTraitStubOnAppEngine($m); + + $res = $trait->runProjectFromAppEngine(); + + $this->assertEquals($res, $projectId); + } + + public function testDetectProjectIdWithEnvVar() + { + $projectId = 'appengineflex-project-rawks'; + + // set a test environment variable + putenv('GOOGLE_DETECT_PROJECT_TEST_PROJECT_ID=' . $projectId); + + $trait = new DetectProjectTraitStubWithEnvVar(); + + $res = $trait->runProjectFromEnvVar(); + + // remove the environment variable + putenv('GOOGLE_DETECT_PROJECT_TEST_PROJECT_ID'); + + $this->assertEquals($res, $projectId); + } + + public function testDetectProjectIdWithGcloudConfig() + { + $projectId = 'gcloud-project-rawks'; + + $configPath = __DIR__ . '/fixtures/config_default_fixture'; + + $trait = new DetectProjectTraitStubWithGcloudConfig($configPath); + + $res = $trait->runProjectFromGcloudConfig(); + + $this->assertEquals($res, $projectId); + } +} + +class DetectProjectTraitStub +{ + use DetectProjectTrait; + + public function runPathToGcloudConfig() + { + return $this->pathToGcloudConfig(); + } + + public function runProjectFromEnvVar() + { + return $this->projectFromEnvVar(); + } + + public function runProjectFromGce() + { + return $this->projectFromGce(); + } + + public function runProjectFromAppEngine() + { + return $this->projectFromAppEngine(); + } + + public function runProjectFromGcloudConfig() + { + return $this->projectFromGcloudConfig(); + } +} + +class DetectProjectTraitStubOnWindows extends DetectProjectTraitStub +{ + use DetectProjectTrait; + + public function onWindows() + { + return true; + } +} + +class DetectProjectTraitStubWithEnvVar extends DetectProjectTraitStub +{ + use DetectProjectTrait; + + protected function getEnvVar() + { + return 'GOOGLE_DETECT_PROJECT_TEST_PROJECT_ID'; + } +} + +class DetectProjectTraitStubOnAppEngine extends DetectProjectTraitStub +{ + use DetectProjectTrait; + + private $appIdentity; + + public function __construct($appIdentity) + { + $this->appIdentity = $appIdentity; + } + + protected function onAppEngine() + { + return true; + } + + protected function getAppIdentity() + { + return $this->appIdentity->reveal(); + } +} + +class DetectProjectTraitStubOnGce extends DetectProjectTraitStub +{ + use DetectProjectTrait; + + private $metadata; + + public function __construct($metadata) + { + $this->metadata = $metadata; + } + + protected function onGce($httpHandler) + { + return true; + } + + protected function getMetadata() + { + return $this->metadata->reveal(); + } +} + +class DetectProjectTraitStubWithGcloudConfig extends DetectProjectTraitStub +{ + use DetectProjectTrait; + + private $configPath; + + public function __construct($configPath) + { + $this->configPath = $configPath; + } + + protected function pathToGcloudConfig() + { + return $this->configPath; + } +} + +class DetectProjectTraitStubGrpcDependencyChecks extends DetectProjectTraitStub +{ + use DetectProjectTrait; + + private $dependencyStatus; + + public function __construct(array $dependencyStatus) + { + $this->dependencyStatus = $dependencyStatus; + } + + protected function getGrpcDependencyStatus() + { + return $this->dependencyStatus; + } +} diff --git a/tests/unit/fixtures/config_default_fixture b/tests/unit/fixtures/config_default_fixture new file mode 100644 index 000000000000..93d73c40f47d --- /dev/null +++ b/tests/unit/fixtures/config_default_fixture @@ -0,0 +1,2 @@ +[core] +project = gcloud-project-rawks \ No newline at end of file