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

Commit

Permalink
feat: support setting ServiceOption for quota project (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
codyoss committed Dec 13, 2019
1 parent b465630 commit 6aa4476
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 24 deletions.
Expand Up @@ -176,6 +176,7 @@ ApiClientHeaderProvider.Builder getInternalHeaderProviderBuilder(
builder.setClientLibToken(
ServiceOptions.getGoogApiClientLibName(),
GaxProperties.getLibraryVersion(serviceOptions.getClass()));
builder.setQuotaProjectIdToken(serviceOptions.getQuotaProjectId());
return builder;
}

Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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<OptionsT> serviceRpcFactory;
private transient ServiceFactory<ServiceT, OptionsT> serviceFactory;
Expand Down Expand Up @@ -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() {}
Expand All @@ -147,6 +150,7 @@ protected Builder(ServiceOptions<ServiceT, OptionsT> options) {
clock = options.clock;
transportOptions = options.transportOptions;
clientLibToken = options.clientLibToken;
quotaProjectId = options.quotaProjectId;
}

protected abstract ServiceOptions<ServiceT, OptionsT> build();
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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 <a href="https://cloud.google.com/apis/docs/system-parameters">See system parameter
* $userProject</a>
*/
public B setQuotaProjectId(String quotaProjectId) {
this.quotaProjectId = quotaProjectId;
return self();
}

protected Set<String> getAllowedClientLibTokens() {
return allowedClientLibTokens;
}
Expand Down Expand Up @@ -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");
}

/**
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -664,7 +687,8 @@ protected int baseHashCode() {
retrySettings,
serviceFactoryClassName,
serviceRpcFactoryClassName,
clock);
clock,
quotaProjectId);
}

protected boolean baseEquals(ServiceOptions<?, ?> other) {
Expand All @@ -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 {
Expand Down Expand Up @@ -734,4 +759,9 @@ public static <T> T getFromServiceLoader(Class<? extends T> 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;
}
}
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand All @@ -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()
Expand All @@ -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();
Expand Down Expand Up @@ -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());
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -152,7 +152,7 @@
<github.global.server>github</github.global.server>
<site.installationModule>google-cloud-core-parent</site.installationModule>

<gax.version>1.51.0</gax.version>
<gax.version>1.52.0</gax.version>
<google.api-common.version>1.8.1</google.api-common.version>
<google.common-protos.version>1.17.0</google.common-protos.version>
<google.iam.version>0.13.0</google.iam.version>
Expand Down

0 comments on commit 6aa4476

Please sign in to comment.