Skip to content
This repository was archived by the owner on Dec 3, 2023. It is now read-only.

Commit 6aa4476

Browse files
authored
feat: support setting ServiceOption for quota project (#92)
1 parent b465630 commit 6aa4476

4 files changed

Lines changed: 122 additions & 24 deletions

File tree

google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ ApiClientHeaderProvider.Builder getInternalHeaderProviderBuilder(
176176
builder.setClientLibToken(
177177
ServiceOptions.getGoogApiClientLibName(),
178178
GaxProperties.getLibraryVersion(serviceOptions.getClass()));
179+
builder.setQuotaProjectIdToken(serviceOptions.getQuotaProjectId());
179180
return builder;
180181
}
181182

google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import com.google.api.gax.rpc.NoHeaderProvider;
4444
import com.google.auth.Credentials;
4545
import com.google.auth.oauth2.GoogleCredentials;
46+
import com.google.auth.oauth2.QuotaProjectIdProvider;
4647
import com.google.auth.oauth2.ServiceAccountCredentials;
4748
import com.google.cloud.spi.ServiceRpcFactory;
4849
import com.google.common.base.Preconditions;
@@ -102,6 +103,7 @@ public abstract class ServiceOptions<
102103
protected Credentials credentials;
103104
private final TransportOptions transportOptions;
104105
private final HeaderProvider headerProvider;
106+
private final String quotaProjectId;
105107

106108
private transient ServiceRpcFactory<OptionsT> serviceRpcFactory;
107109
private transient ServiceFactory<ServiceT, OptionsT> serviceFactory;
@@ -132,6 +134,7 @@ public abstract static class Builder<
132134
private TransportOptions transportOptions;
133135
private HeaderProvider headerProvider;
134136
private String clientLibToken = ServiceOptions.getGoogApiClientLibName();
137+
private String quotaProjectId;
135138

136139
@InternalApi("This class should only be extended within google-cloud-java")
137140
protected Builder() {}
@@ -147,6 +150,7 @@ protected Builder(ServiceOptions<ServiceT, OptionsT> options) {
147150
clock = options.clock;
148151
transportOptions = options.transportOptions;
149152
clientLibToken = options.clientLibToken;
153+
quotaProjectId = options.quotaProjectId;
150154
}
151155

152156
protected abstract ServiceOptions<ServiceT, OptionsT> build();
@@ -212,6 +216,10 @@ public B setCredentials(Credentials credentials) {
212216
if (this.projectId == null && credentials instanceof ServiceAccountCredentials) {
213217
this.projectId = ((ServiceAccountCredentials) credentials).getProjectId();
214218
}
219+
220+
if (this.quotaProjectId == null && credentials instanceof QuotaProjectIdProvider) {
221+
this.quotaProjectId = ((ServiceAccountCredentials) credentials).getQuotaProjectId();
222+
}
215223
return self();
216224
}
217225

@@ -269,6 +277,17 @@ public B setClientLibToken(String clientLibToken) {
269277
return self();
270278
}
271279

280+
/**
281+
* Sets the quotaProjectId that specifies the project used for quota and billing purposes.
282+
*
283+
* @see <a href="https://cloud.google.com/apis/docs/system-parameters">See system parameter
284+
* $userProject</a>
285+
*/
286+
public B setQuotaProjectId(String quotaProjectId) {
287+
this.quotaProjectId = quotaProjectId;
288+
return self();
289+
}
290+
272291
protected Set<String> getAllowedClientLibTokens() {
273292
return allowedClientLibTokens;
274293
}
@@ -305,6 +324,10 @@ protected ServiceOptions(
305324
firstNonNull(builder.transportOptions, serviceDefaults.getDefaultTransportOptions());
306325
headerProvider = firstNonNull(builder.headerProvider, new NoHeaderProvider());
307326
clientLibToken = builder.clientLibToken;
327+
quotaProjectId =
328+
builder.quotaProjectId != null
329+
? builder.quotaProjectId
330+
: getValueFromCredentialsFile(System.getenv(CREDENTIAL_ENV_NAME), "quota_project_id");
308331
}
309332

310333
/**
@@ -488,24 +511,24 @@ static boolean headerContainsMetadataFlavor(HttpResponse response) {
488511
}
489512

490513
protected static String getServiceAccountProjectId() {
491-
return getServiceAccountProjectId(System.getenv(CREDENTIAL_ENV_NAME));
514+
return getValueFromCredentialsFile(System.getenv(CREDENTIAL_ENV_NAME), "project_id");
492515
}
493516

494517
@InternalApi("Visible for testing")
495-
static String getServiceAccountProjectId(String credentialsPath) {
496-
String project = null;
518+
static String getValueFromCredentialsFile(String credentialsPath, String key) {
519+
String value = null;
497520
if (credentialsPath != null) {
498521
try (InputStream credentialsStream = new FileInputStream(credentialsPath)) {
499522
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
500523
JsonObjectParser parser = new JsonObjectParser(jsonFactory);
501524
GenericJson fileContents =
502525
parser.parseAndClose(credentialsStream, Charsets.UTF_8, GenericJson.class);
503-
project = (String) fileContents.get("project_id");
526+
value = (String) fileContents.get(key);
504527
} catch (IOException e) {
505528
// ignore
506529
}
507530
}
508-
return project;
531+
return value;
509532
}
510533

511534
/**
@@ -664,7 +687,8 @@ protected int baseHashCode() {
664687
retrySettings,
665688
serviceFactoryClassName,
666689
serviceRpcFactoryClassName,
667-
clock);
690+
clock,
691+
quotaProjectId);
668692
}
669693

670694
protected boolean baseEquals(ServiceOptions<?, ?> other) {
@@ -674,7 +698,8 @@ protected boolean baseEquals(ServiceOptions<?, ?> other) {
674698
&& Objects.equals(retrySettings, other.retrySettings)
675699
&& Objects.equals(serviceFactoryClassName, other.serviceFactoryClassName)
676700
&& Objects.equals(serviceRpcFactoryClassName, other.serviceRpcFactoryClassName)
677-
&& Objects.equals(clock, clock);
701+
&& Objects.equals(clock, clock)
702+
&& Objects.equals(quotaProjectId, other.quotaProjectId);
678703
}
679704

680705
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
@@ -734,4 +759,9 @@ public static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInst
734759
public String getClientLibToken() {
735760
return clientLibToken;
736761
}
762+
763+
/** Returns the quotaProjectId that specifies the project used for quota and billing purposes. */
764+
public String getQuotaProjectId() {
765+
return quotaProjectId;
766+
}
737767
}

google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
import org.junit.rules.ExpectedException;
5454

5555
public class ServiceOptionsTest {
56+
private static GoogleCredentials credentials;
57+
private static GoogleCredentials credentialsWithProjectId;
58+
private static GoogleCredentials credentialsWithQuotaProject;
59+
5660
private static final String JSON_KEY =
5761
"{\n"
5862
+ " \"private_key_id\": \"somekeyid\",\n"
@@ -80,16 +84,6 @@ public class ServiceOptionsTest {
8084
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
8185
+ " \"type\": \"service_account\"\n"
8286
+ "}";
83-
private static GoogleCredentials credentials;
84-
85-
static {
86-
try {
87-
InputStream keyStream = new ByteArrayInputStream(JSON_KEY.getBytes());
88-
credentials = GoogleCredentials.fromStream(keyStream);
89-
} catch (IOException e) {
90-
fail("Couldn't create fake JSON credentials.");
91-
}
92-
}
9387

9488
private static final String JSON_KEY_PROJECT_ID =
9589
"{\n"
@@ -119,15 +113,51 @@ public class ServiceOptionsTest {
119113
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
120114
+ " \"type\": \"service_account\"\n"
121115
+ "}";
122-
private static GoogleCredentials credentialsWithProjectId;
116+
117+
private static final String JSON_KEY_QUOTA_PROJECT_ID =
118+
"{\n"
119+
+ " \"private_key_id\": \"somekeyid\",\n"
120+
+ " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS"
121+
+ "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg"
122+
+ "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4"
123+
+ "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2"
124+
+ "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa"
125+
+ "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF"
126+
+ "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL"
127+
+ "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\"
128+
+ "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp"
129+
+ "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF"
130+
+ "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm"
131+
+ "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK"
132+
+ "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF"
133+
+ "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR"
134+
+ "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl"
135+
+ "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa"
136+
+ "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi"
137+
+ "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG"
138+
+ "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk"
139+
+ "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n"
140+
+ " \"project_id\": \"someprojectid\",\n"
141+
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
142+
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
143+
+ " \"type\": \"service_account\",\n"
144+
+ " \"quota_project_id\": \"some-quota-project-id\"\n"
145+
+ "}";
123146

124147
static {
148+
credentials = loadCredentials(JSON_KEY);
149+
credentialsWithProjectId = loadCredentials(JSON_KEY_PROJECT_ID);
150+
credentialsWithQuotaProject = loadCredentials(JSON_KEY_QUOTA_PROJECT_ID);
151+
}
152+
153+
static GoogleCredentials loadCredentials(String credentialFile) {
125154
try {
126-
InputStream keyStream = new ByteArrayInputStream(JSON_KEY_PROJECT_ID.getBytes());
127-
credentialsWithProjectId = GoogleCredentials.fromStream(keyStream);
155+
InputStream keyStream = new ByteArrayInputStream(credentialFile.getBytes());
156+
return GoogleCredentials.fromStream(keyStream);
128157
} catch (IOException e) {
129158
fail("Couldn't create fake JSON credentials.");
130159
}
160+
return null;
131161
}
132162

133163
private static final ApiClock TEST_CLOCK = new TestClock();
@@ -138,6 +168,7 @@ public class ServiceOptionsTest {
138168
.setHost("host")
139169
.setProjectId("project-id")
140170
.setRetrySettings(ServiceOptions.getNoRetrySettings())
171+
.setQuotaProjectId("quota-project-id")
141172
.build();
142173
private static final TestServiceOptions OPTIONS_NO_CREDENTIALS =
143174
TestServiceOptions.newBuilder()
@@ -146,6 +177,7 @@ public class ServiceOptionsTest {
146177
.setHost("host")
147178
.setProjectId("project-id")
148179
.setRetrySettings(ServiceOptions.getNoRetrySettings())
180+
.setQuotaProjectId("quota-project-id")
149181
.build();
150182
private static final TestServiceOptions DEFAULT_OPTIONS =
151183
TestServiceOptions.newBuilder().setProjectId("project-id").build();
@@ -283,6 +315,39 @@ public void testBuilder() {
283315
assertSame(ServiceOptions.getDefaultRetrySettings(), DEFAULT_OPTIONS.getRetrySettings());
284316
}
285317

318+
@Test
319+
public void testBuilder_quotaProjectServiceOptionTakesPrecedence() {
320+
TestServiceOptions noCredsWithQuotaProject =
321+
TestServiceOptions.newBuilder()
322+
.setCredentials(NoCredentials.getInstance())
323+
.setProjectId("project-id")
324+
.setQuotaProjectId("quota-project-id")
325+
.build();
326+
TestServiceOptions quotaProjectCredsWithQuotaProject =
327+
TestServiceOptions.newBuilder()
328+
.setQuotaProjectId("quota-project-id")
329+
.setCredentials(credentialsWithQuotaProject)
330+
.build();
331+
TestServiceOptions quotaProjectCredsWithQuotaProject2 =
332+
TestServiceOptions.newBuilder()
333+
.setCredentials(credentialsWithQuotaProject)
334+
.setQuotaProjectId("quota-project-id")
335+
.build();
336+
TestServiceOptions quotaProjectCreds =
337+
TestServiceOptions.newBuilder().setCredentials(credentialsWithQuotaProject).build();
338+
TestServiceOptions none =
339+
TestServiceOptions.newBuilder()
340+
.setCredentials(NoCredentials.getInstance())
341+
.setProjectId("project-id")
342+
.build();
343+
344+
assertEquals("quota-project-id", noCredsWithQuotaProject.getQuotaProjectId());
345+
assertEquals("quota-project-id", quotaProjectCredsWithQuotaProject.getQuotaProjectId());
346+
assertEquals("quota-project-id", quotaProjectCredsWithQuotaProject2.getQuotaProjectId());
347+
assertEquals("some-quota-project-id", quotaProjectCreds.getQuotaProjectId());
348+
assertEquals(null, none.getQuotaProjectId());
349+
}
350+
286351
@Test
287352
public void testBuilderNoCredentials() {
288353
assertEquals(NoCredentials.getInstance(), OPTIONS_NO_CREDENTIALS.getCredentials());
@@ -293,6 +358,7 @@ public void testBuilderNoCredentials() {
293358
assertEquals("host", OPTIONS_NO_CREDENTIALS.getHost());
294359
assertEquals("project-id", OPTIONS_NO_CREDENTIALS.getProjectId());
295360
assertSame(ServiceOptions.getNoRetrySettings(), OPTIONS_NO_CREDENTIALS.getRetrySettings());
361+
assertEquals("quota-project-id", OPTIONS.getQuotaProjectId());
296362
}
297363

298364
@Test
@@ -372,7 +438,8 @@ public void testGetServiceAccountProjectId() throws Exception {
372438
Files.write("{\"project_id\":\"my-project-id\"}".getBytes(), credentialsFile);
373439

374440
assertEquals(
375-
"my-project-id", ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath()));
441+
"my-project-id",
442+
ServiceOptions.getValueFromCredentialsFile(credentialsFile.getPath(), "project_id"));
376443
}
377444

378445
@Test
@@ -381,14 +448,14 @@ public void testGetServiceAccountProjectId_badJson() throws Exception {
381448
credentialsFile.deleteOnExit();
382449
Files.write("asdfghj".getBytes(), credentialsFile);
383450

384-
assertNull(ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath()));
451+
assertNull(ServiceOptions.getValueFromCredentialsFile(credentialsFile.getPath(), "project_id"));
385452
}
386453

387454
@Test
388455
public void testGetServiceAccountProjectId_nonExistentFile() throws Exception {
389456
File credentialsFile = new File("/doesnotexist");
390457

391-
assertNull(ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath()));
458+
assertNull(ServiceOptions.getValueFromCredentialsFile(credentialsFile.getPath(), "project_id"));
392459
}
393460

394461
@Test

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
<github.global.server>github</github.global.server>
153153
<site.installationModule>google-cloud-core-parent</site.installationModule>
154154

155-
<gax.version>1.51.0</gax.version>
155+
<gax.version>1.52.0</gax.version>
156156
<google.api-common.version>1.8.1</google.api-common.version>
157157
<google.common-protos.version>1.17.0</google.common-protos.version>
158158
<google.iam.version>0.13.0</google.iam.version>

0 commit comments

Comments
 (0)