diff --git a/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java b/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java index 2dbcb018f8..4bdfee3cca 100644 --- a/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java +++ b/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java @@ -176,6 +176,7 @@ ApiClientHeaderProvider.Builder getInternalHeaderProviderBuilder( builder.setClientLibToken( ServiceOptions.getGoogApiClientLibName(), GaxProperties.getLibraryVersion(serviceOptions.getClass())); + builder.setQuotaProjectIdToken(serviceOptions.getQuotaProjectId()); return builder; } diff --git a/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java b/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java index 56f2d19595..792609f4b4 100644 --- a/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java +++ b/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java @@ -43,6 +43,7 @@ import com.google.api.gax.rpc.NoHeaderProvider; import com.google.auth.Credentials; import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.QuotaProjectIdProvider; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.spi.ServiceRpcFactory; import com.google.common.base.Preconditions; @@ -102,6 +103,7 @@ public abstract class ServiceOptions< protected Credentials credentials; private final TransportOptions transportOptions; private final HeaderProvider headerProvider; + private final String quotaProjectId; private transient ServiceRpcFactory serviceRpcFactory; private transient ServiceFactory serviceFactory; @@ -132,6 +134,7 @@ public abstract static class Builder< private TransportOptions transportOptions; private HeaderProvider headerProvider; private String clientLibToken = ServiceOptions.getGoogApiClientLibName(); + private String quotaProjectId; @InternalApi("This class should only be extended within google-cloud-java") protected Builder() {} @@ -147,6 +150,7 @@ protected Builder(ServiceOptions options) { clock = options.clock; transportOptions = options.transportOptions; clientLibToken = options.clientLibToken; + quotaProjectId = options.quotaProjectId; } protected abstract ServiceOptions build(); @@ -212,6 +216,10 @@ public B setCredentials(Credentials credentials) { if (this.projectId == null && credentials instanceof ServiceAccountCredentials) { this.projectId = ((ServiceAccountCredentials) credentials).getProjectId(); } + + if (this.quotaProjectId == null && credentials instanceof QuotaProjectIdProvider) { + this.quotaProjectId = ((ServiceAccountCredentials) credentials).getQuotaProjectId(); + } return self(); } @@ -269,6 +277,17 @@ public B setClientLibToken(String clientLibToken) { return self(); } + /** + * Sets the quotaProjectId that specifies the project used for quota and billing purposes. + * + * @see See system parameter + * $userProject + */ + public B setQuotaProjectId(String quotaProjectId) { + this.quotaProjectId = quotaProjectId; + return self(); + } + protected Set getAllowedClientLibTokens() { return allowedClientLibTokens; } @@ -305,6 +324,10 @@ protected ServiceOptions( firstNonNull(builder.transportOptions, serviceDefaults.getDefaultTransportOptions()); headerProvider = firstNonNull(builder.headerProvider, new NoHeaderProvider()); clientLibToken = builder.clientLibToken; + quotaProjectId = + builder.quotaProjectId != null + ? builder.quotaProjectId + : getValueFromCredentialsFile(System.getenv(CREDENTIAL_ENV_NAME), "quota_project_id"); } /** @@ -488,24 +511,24 @@ static boolean headerContainsMetadataFlavor(HttpResponse response) { } protected static String getServiceAccountProjectId() { - return getServiceAccountProjectId(System.getenv(CREDENTIAL_ENV_NAME)); + return getValueFromCredentialsFile(System.getenv(CREDENTIAL_ENV_NAME), "project_id"); } @InternalApi("Visible for testing") - static String getServiceAccountProjectId(String credentialsPath) { - String project = null; + static String getValueFromCredentialsFile(String credentialsPath, String key) { + String value = null; if (credentialsPath != null) { try (InputStream credentialsStream = new FileInputStream(credentialsPath)) { JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); JsonObjectParser parser = new JsonObjectParser(jsonFactory); GenericJson fileContents = parser.parseAndClose(credentialsStream, Charsets.UTF_8, GenericJson.class); - project = (String) fileContents.get("project_id"); + value = (String) fileContents.get(key); } catch (IOException e) { // ignore } } - return project; + return value; } /** @@ -664,7 +687,8 @@ protected int baseHashCode() { retrySettings, serviceFactoryClassName, serviceRpcFactoryClassName, - clock); + clock, + quotaProjectId); } protected boolean baseEquals(ServiceOptions other) { @@ -674,7 +698,8 @@ protected boolean baseEquals(ServiceOptions other) { && Objects.equals(retrySettings, other.retrySettings) && Objects.equals(serviceFactoryClassName, other.serviceFactoryClassName) && Objects.equals(serviceRpcFactoryClassName, other.serviceRpcFactoryClassName) - && Objects.equals(clock, clock); + && Objects.equals(clock, clock) + && Objects.equals(quotaProjectId, other.quotaProjectId); } private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { @@ -734,4 +759,9 @@ public static T getFromServiceLoader(Class clazz, T defaultInst public String getClientLibToken() { return clientLibToken; } + + /** Returns the quotaProjectId that specifies the project used for quota and billing purposes. */ + public String getQuotaProjectId() { + return quotaProjectId; + } } diff --git a/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java b/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java index 3ce9ffd543..8f8cab08b3 100644 --- a/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java +++ b/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java @@ -53,6 +53,10 @@ import org.junit.rules.ExpectedException; public class ServiceOptionsTest { + private static GoogleCredentials credentials; + private static GoogleCredentials credentialsWithProjectId; + private static GoogleCredentials credentialsWithQuotaProject; + private static final String JSON_KEY = "{\n" + " \"private_key_id\": \"somekeyid\",\n" @@ -80,16 +84,6 @@ public class ServiceOptionsTest { + " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n" + " \"type\": \"service_account\"\n" + "}"; - private static GoogleCredentials credentials; - - static { - try { - InputStream keyStream = new ByteArrayInputStream(JSON_KEY.getBytes()); - credentials = GoogleCredentials.fromStream(keyStream); - } catch (IOException e) { - fail("Couldn't create fake JSON credentials."); - } - } private static final String JSON_KEY_PROJECT_ID = "{\n" @@ -119,15 +113,51 @@ public class ServiceOptionsTest { + " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n" + " \"type\": \"service_account\"\n" + "}"; - private static GoogleCredentials credentialsWithProjectId; + + private static final String JSON_KEY_QUOTA_PROJECT_ID = + "{\n" + + " \"private_key_id\": \"somekeyid\",\n" + + " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS" + + "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg" + + "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4" + + "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2" + + "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa" + + "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF" + + "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL" + + "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\" + + "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp" + + "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF" + + "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm" + + "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK" + + "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF" + + "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR" + + "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl" + + "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa" + + "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi" + + "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG" + + "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk" + + "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n" + + " \"project_id\": \"someprojectid\",\n" + + " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n" + + " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n" + + " \"type\": \"service_account\",\n" + + " \"quota_project_id\": \"some-quota-project-id\"\n" + + "}"; static { + credentials = loadCredentials(JSON_KEY); + credentialsWithProjectId = loadCredentials(JSON_KEY_PROJECT_ID); + credentialsWithQuotaProject = loadCredentials(JSON_KEY_QUOTA_PROJECT_ID); + } + + static GoogleCredentials loadCredentials(String credentialFile) { try { - InputStream keyStream = new ByteArrayInputStream(JSON_KEY_PROJECT_ID.getBytes()); - credentialsWithProjectId = GoogleCredentials.fromStream(keyStream); + InputStream keyStream = new ByteArrayInputStream(credentialFile.getBytes()); + return GoogleCredentials.fromStream(keyStream); } catch (IOException e) { fail("Couldn't create fake JSON credentials."); } + return null; } private static final ApiClock TEST_CLOCK = new TestClock(); @@ -138,6 +168,7 @@ public class ServiceOptionsTest { .setHost("host") .setProjectId("project-id") .setRetrySettings(ServiceOptions.getNoRetrySettings()) + .setQuotaProjectId("quota-project-id") .build(); private static final TestServiceOptions OPTIONS_NO_CREDENTIALS = TestServiceOptions.newBuilder() @@ -146,6 +177,7 @@ public class ServiceOptionsTest { .setHost("host") .setProjectId("project-id") .setRetrySettings(ServiceOptions.getNoRetrySettings()) + .setQuotaProjectId("quota-project-id") .build(); private static final TestServiceOptions DEFAULT_OPTIONS = TestServiceOptions.newBuilder().setProjectId("project-id").build(); @@ -283,6 +315,39 @@ public void testBuilder() { assertSame(ServiceOptions.getDefaultRetrySettings(), DEFAULT_OPTIONS.getRetrySettings()); } + @Test + public void testBuilder_quotaProjectServiceOptionTakesPrecedence() { + TestServiceOptions noCredsWithQuotaProject = + TestServiceOptions.newBuilder() + .setCredentials(NoCredentials.getInstance()) + .setProjectId("project-id") + .setQuotaProjectId("quota-project-id") + .build(); + TestServiceOptions quotaProjectCredsWithQuotaProject = + TestServiceOptions.newBuilder() + .setQuotaProjectId("quota-project-id") + .setCredentials(credentialsWithQuotaProject) + .build(); + TestServiceOptions quotaProjectCredsWithQuotaProject2 = + TestServiceOptions.newBuilder() + .setCredentials(credentialsWithQuotaProject) + .setQuotaProjectId("quota-project-id") + .build(); + TestServiceOptions quotaProjectCreds = + TestServiceOptions.newBuilder().setCredentials(credentialsWithQuotaProject).build(); + TestServiceOptions none = + TestServiceOptions.newBuilder() + .setCredentials(NoCredentials.getInstance()) + .setProjectId("project-id") + .build(); + + assertEquals("quota-project-id", noCredsWithQuotaProject.getQuotaProjectId()); + assertEquals("quota-project-id", quotaProjectCredsWithQuotaProject.getQuotaProjectId()); + assertEquals("quota-project-id", quotaProjectCredsWithQuotaProject2.getQuotaProjectId()); + assertEquals("some-quota-project-id", quotaProjectCreds.getQuotaProjectId()); + assertEquals(null, none.getQuotaProjectId()); + } + @Test public void testBuilderNoCredentials() { assertEquals(NoCredentials.getInstance(), OPTIONS_NO_CREDENTIALS.getCredentials()); @@ -293,6 +358,7 @@ public void testBuilderNoCredentials() { assertEquals("host", OPTIONS_NO_CREDENTIALS.getHost()); assertEquals("project-id", OPTIONS_NO_CREDENTIALS.getProjectId()); assertSame(ServiceOptions.getNoRetrySettings(), OPTIONS_NO_CREDENTIALS.getRetrySettings()); + assertEquals("quota-project-id", OPTIONS.getQuotaProjectId()); } @Test @@ -372,7 +438,8 @@ public void testGetServiceAccountProjectId() throws Exception { Files.write("{\"project_id\":\"my-project-id\"}".getBytes(), credentialsFile); assertEquals( - "my-project-id", ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath())); + "my-project-id", + ServiceOptions.getValueFromCredentialsFile(credentialsFile.getPath(), "project_id")); } @Test @@ -381,14 +448,14 @@ public void testGetServiceAccountProjectId_badJson() throws Exception { credentialsFile.deleteOnExit(); Files.write("asdfghj".getBytes(), credentialsFile); - assertNull(ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath())); + assertNull(ServiceOptions.getValueFromCredentialsFile(credentialsFile.getPath(), "project_id")); } @Test public void testGetServiceAccountProjectId_nonExistentFile() throws Exception { File credentialsFile = new File("/doesnotexist"); - assertNull(ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath())); + assertNull(ServiceOptions.getValueFromCredentialsFile(credentialsFile.getPath(), "project_id")); } @Test diff --git a/pom.xml b/pom.xml index cc83f647ff..5ef6385bc7 100644 --- a/pom.xml +++ b/pom.xml @@ -152,7 +152,7 @@ github google-cloud-core-parent - 1.51.0 + 1.52.0 1.8.1 1.17.0 0.13.0