Skip to content

Commit d11f8a6

Browse files
authored
feat(Spanner): add universe domain support (#8177)
1 parent b424ef9 commit d11f8a6

4 files changed

Lines changed: 274 additions & 2 deletions

File tree

Spanner/src/Connection/Grpc.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Google\ApiCore\Call;
2121
use Google\ApiCore\CredentialsWrapper;
2222
use Google\ApiCore\Serializer;
23+
use Google\Auth\GetUniverseDomainInterface;
2324
use Google\Cloud\Core\EmulatorTrait;
2425
use Google\Cloud\Core\GrpcRequestWrapper;
2526
use Google\Cloud\Core\GrpcTrait;
@@ -278,12 +279,17 @@ public function __construct(array $config = [])
278279
ManualSpannerClient::VERSION,
279280
isset($config['authHttpHandler'])
280281
? $config['authHttpHandler']
281-
: null
282+
: null,
283+
$config['universeDomain'] ?? null
282284
);
283285

284286
$config += [
285287
'emulatorHost' => null,
286-
'queryOptions' => []
288+
'queryOptions' => [],
289+
// If the user has not supplied a universe domain, use the environment variable if set.
290+
// Otherwise, use the default ("googleapis.com").
291+
'universeDomain' => getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN')
292+
?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN,
287293
];
288294
if ((bool) $config['emulatorHost']) {
289295
$grpcConfig = array_merge(
@@ -293,6 +299,12 @@ public function __construct(array $config = [])
293299
} elseif (isset($config['apiEndpoint'])) {
294300
$grpcConfig['apiEndpoint'] = $config['apiEndpoint'];
295301
}
302+
303+
// configure the universe domain if set
304+
if (isset($config['universeDomain'])) {
305+
$grpcConfig['universeDomain'] = $config['universeDomain'];
306+
}
307+
296308
$this->credentialsWrapper = $grpcConfig['credentials'];
297309

298310
$this->defaultQueryOptions = $config['queryOptions'];

Spanner/src/SpannerClient.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ class SpannerClient
199199
* {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value.
200200
* @type bool $routeToLeader Enable/disable Leader Aware Routing.
201201
* **Defaults to** `true` (enabled).
202+
* @type string $universeDomain The expected universe of the credentials. Defaults to
203+
* "googleapis.com"
202204
* }
203205
* @throws GoogleException If the gRPC extension is not enabled.
204206
*/
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Google\Cloud\Spanner\Tests\System;
19+
20+
use Google\Cloud\Core\Testing\System\SystemTestCase;
21+
use Google\Cloud\Core\LongRunning\LongRunningOperation;
22+
use Google\Cloud\Spanner\SpannerClient;
23+
use Google\Cloud\Spanner\KeySet;
24+
25+
class UniverseDomainTest extends SystemTestCase
26+
{
27+
private static $client;
28+
private static $instance;
29+
private static $instanceId;
30+
private static $database;
31+
private static $dbName;
32+
private static $tableName;
33+
34+
/**
35+
* @beforeClass
36+
*/
37+
public static function setUpTestFixtures(): void
38+
{
39+
if (!$keyFilePath = getenv('GOOGLE_CLOUD_PHP_TESTS_UNIVERSE_DOMAIN_KEY_PATH')) {
40+
self::markTestSkipped('Set GOOGLE_CLOUD_PHP_TESTS_UNIVERSE_DOMAIN_KEY_PATH to run system tests');
41+
}
42+
43+
$credentials = json_decode(file_get_contents($keyFilePath), true);
44+
if (!isset($credentials['universe_domain'])) {
45+
throw new \Exception('The provided key file does not contain universe domain credentials');
46+
}
47+
48+
self::$client = new SpannerClient([
49+
'keyFilePath' => $keyFilePath,
50+
'projectId' => $credentials['project_id'] ?? null,
51+
'universeDomain' => $credentials['universe_domain'] ?? null
52+
]);
53+
54+
// Create a unique instance ID for this test
55+
self::$instanceId = uniqid(SpannerTestCase::INSTANCE_NAME);
56+
self::$dbName = uniqid(SpannerTestCase::TESTING_PREFIX);
57+
self::$tableName = uniqid(SpannerTestCase::TESTING_PREFIX);
58+
}
59+
60+
/**
61+
* Test creating an instance with universe domain credentials
62+
*/
63+
public function testCreateInstanceWithUniverseDomain()
64+
{
65+
// Get the first available instance configuration
66+
$configs = iterator_to_array(self::$client->instanceConfigurations());
67+
$this->assertNotEmpty($configs, 'No instance configurations found');
68+
$config = $configs[0];
69+
70+
// Create the instance
71+
$op = self::$client->createInstance($config, self::$instanceId, [
72+
'displayName' => 'Universe Domain Test Instance',
73+
'nodeCount' => 1
74+
]);
75+
$op->pollUntilComplete();
76+
77+
$this->assertEquals(LongRunningOperation::STATE_SUCCESS, $op->state());
78+
79+
self::$instance = self::$client->instance(self::$instanceId);
80+
$info = self::$instance->info();
81+
82+
$this->assertStringEndsWith('/' . self::$instanceId, $info['name']);
83+
$this->assertEquals('Universe Domain Test Instance', $info['displayName']);
84+
}
85+
86+
/**
87+
* Test creating a database with universe domain credentials
88+
*
89+
* @depends testCreateInstanceWithUniverseDomain
90+
*/
91+
public function testCreateDatabaseWithUniverseDomain()
92+
{
93+
$op = self::$instance->createDatabase(self::$dbName);
94+
$op->pollUntilComplete();
95+
96+
self::$database = self::$instance->database(self::$dbName);
97+
$this->assertStringEndsWith('/' . self::$dbName, self::$database->name());
98+
99+
// Create a test table
100+
$op = self::$database->updateDdlBatch([
101+
'CREATE TABLE ' . self::$tableName . ' (
102+
id INT64 NOT NULL,
103+
name STRING(MAX) NOT NULL
104+
) PRIMARY KEY (id)'
105+
]);
106+
$op->pollUntilComplete();
107+
108+
// Verify the table was created
109+
$result = self::$database->execute(
110+
"SELECT table_name as name FROM information_schema.tables WHERE table_catalog = '' AND table_schema = ''"
111+
);
112+
$tableNames = array_map(fn ($row) => $row['name'], iterator_to_array($result->rows()));
113+
114+
$this->assertContains(self::$tableName, $tableNames);
115+
}
116+
117+
/**
118+
* Test writing and reading data with universe domain credentials.
119+
*
120+
* @depends testCreateDatabaseWithUniverseDomain
121+
*/
122+
public function testReadWriteWithUniverseDomain()
123+
{
124+
// Insert data
125+
$data = [
126+
[
127+
'id' => 1,
128+
'name' => 'John Doe'
129+
],
130+
[
131+
'id' => 2,
132+
'name' => 'Jane Smith'
133+
]
134+
];
135+
136+
self::$database->transaction(['singleUse' => true])
137+
->insertBatch(self::$tableName, $data)
138+
->commit();
139+
140+
// Read data
141+
$results = self::$database->execute('SELECT * FROM ' . self::$tableName . ' ORDER BY id');
142+
$rows = iterator_to_array($results);
143+
144+
$this->assertCount(2, $rows);
145+
$this->assertEquals(1, $rows[0]['id']);
146+
$this->assertEquals('John Doe', $rows[0]['name']);
147+
$this->assertEquals(2, $rows[1]['id']);
148+
$this->assertEquals('Jane Smith', $rows[1]['name']);
149+
}
150+
151+
/**
152+
* Test updating data with universe domain credentials.
153+
*
154+
* @depends testReadWriteWithUniverseDomain
155+
*/
156+
public function testUpdateWithUniverseDomain()
157+
{
158+
// Update data
159+
self::$database->updateBatch(self::$tableName, [
160+
[
161+
'id' => 1,
162+
'name' => 'John Updated'
163+
]
164+
]);
165+
166+
// Read updated data
167+
$results = self::$database->execute('SELECT * FROM ' . self::$tableName . ' WHERE id = 1');
168+
$rows = iterator_to_array($results);
169+
170+
$this->assertCount(1, $rows);
171+
$this->assertEquals('John Updated', $rows[0]['name']);
172+
}
173+
174+
/**
175+
* Test deleting data with universe domain credentials.
176+
*
177+
* @depends testUpdateWithUniverseDomain
178+
*/
179+
public function testDeleteWithUniverseDomain()
180+
{
181+
// Delete data
182+
$keySet = new KeySet([
183+
'keys' => [[1]]
184+
]);
185+
self::$database->delete(self::$tableName, $keySet);
186+
187+
// Verify deletion
188+
$results = self::$database->execute('SELECT * FROM ' . self::$tableName);
189+
$rows = iterator_to_array($results);
190+
191+
$this->assertCount(1, $rows);
192+
$this->assertEquals(2, $rows[0]['id']);
193+
}
194+
195+
/**
196+
* Test dropping the database with universe domain credentials.
197+
*
198+
* @depends testDeleteWithUniverseDomain
199+
*/
200+
public function testDropDatabaseWithUniverseDomain()
201+
{
202+
self::$database->drop();
203+
204+
// Verify the database was dropped
205+
$databases = iterator_to_array(self::$instance->databases());
206+
$dbNames = array_map(fn ($db) => $db->name(), $databases);
207+
208+
$this->assertNotContains(self::$dbName, $dbNames);
209+
}
210+
211+
/**
212+
* Test deleting the instance with universe domain credentials.
213+
*
214+
* @depends testDropDatabaseWithUniverseDomain
215+
*/
216+
public function testDeleteInstanceWithUniverseDomain()
217+
{
218+
self::$instance->delete();
219+
220+
// Verify the instance was deleted
221+
$instances = iterator_to_array(self::$client->instances());
222+
$instanceIds = array_map(fn ($instance) => $instance->name(), $instances);
223+
224+
$this->assertNotContains(self::$instanceId, $instanceIds);
225+
}
226+
}

Spanner/tests/Unit/Connection/GrpcTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,38 @@ public function testApiEndpoint()
109109
$this->assertEquals($expected, $grpc->config['apiEndpoint']);
110110
}
111111

112+
/**
113+
* @dataProvider clientUniverseDomainConfigProvider
114+
*/
115+
public function testUniverseDomain($config, $expectedUniverseDomain, ?string $envUniverse = null)
116+
{
117+
if ($envUniverse) {
118+
putenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN=' . $envUniverse);
119+
}
120+
121+
$grpc = new GrpcStub($config);
122+
123+
if ($envUniverse) {
124+
// We have to do this instead of using "@runInSeparateProcess" because in the case of
125+
// an error, PHPUnit throws a "Serialization of 'ReflectionClass' is not allowed" error.
126+
// @TODO: Remove this once we've updated to PHPUnit 10.
127+
putenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN');
128+
}
129+
130+
$this->assertEquals($expectedUniverseDomain, $grpc->config['universeDomain']);
131+
}
132+
133+
public function clientUniverseDomainConfigProvider()
134+
{
135+
return [
136+
[[], 'googleapis.com'],
137+
[['universeDomain' => 'googleapis.com'], 'googleapis.com'],
138+
[['universeDomain' => 'abc.def.ghi'], 'abc.def.ghi'],
139+
[[], 'abc.def.ghi', 'abc.def.ghi'],
140+
[['universeDomain' => 'googleapis.com'], 'googleapis.com', 'abc.def.ghi'],
141+
];
142+
}
143+
112144
public function testListInstanceConfigs()
113145
{
114146
$this->assertCallCorrect('listInstanceConfigs', [

0 commit comments

Comments
 (0)