Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public class ComputeEngineCredentials extends GoogleCredentials
private transient HttpTransportFactory transportFactory;

private String universeDomainFromMetadata = null;
private String projectId = null;

/**
* Experimental Feature.
Expand Down Expand Up @@ -340,6 +341,76 @@ private String getUniverseDomainFromMetadata() throws IOException {
return responseString;
}

/**
* Retrieves the Google Cloud project ID from the Compute Engine (GCE) metadata server.
*
* <p>On its first successful execution, it fetches the project ID and caches it for the lifetime
* of the object. Subsequent calls will return the cached value without making additional network
* requests.
*
* <p>If the request to the metadata server fails (e.g., due to network issues, or if the VM lacks
* the required service account permissions), the method will attempt to fall back to a default
* project ID provider which could be {@code null}.
*
* @return the GCP project ID string, or {@code null} if the metadata server is inaccessible and
* no fallback project ID can be determined.
*/
@Override
public String getProjectId() {
synchronized (this) {
if (this.projectId != null) {
return this.projectId;
}
}

String projectIdFromMetadata = getProjectIdFromMetadata();
synchronized (this) {
this.projectId = projectIdFromMetadata;
}
Comment on lines +367 to +369
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a check that we only set this if projectIdFromMetadata is not null.

I think one small possibility is is that if there are multiple getProjectId() calls at the same time and all but the last one work. Last one errors out and resets this to null.

I don't think we need to worry about trying to ensure that only one projectId call ever goes.

return projectIdFromMetadata;
}

private String getProjectIdFromMetadata() {
try {
HttpResponse response = getMetadataResponse(getProjectIdUrl(), RequestType.UNTRACKED, false);
int statusCode = response.getStatusCode();
if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
LoggingUtils.log(
LOGGER_PROVIDER,
Level.WARNING,
Collections.emptyMap(),
String.format(
"Error code %s trying to get project ID from"
+ " Compute Engine metadata. This may be because the virtual machine instance"
+ " does not have permission scopes specified.",
statusCode));
return super.getProjectId();
}
if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
LoggingUtils.log(
LOGGER_PROVIDER,
Level.WARNING,
Collections.emptyMap(),
String.format(
"Unexpected Error code %s trying to get project ID"
+ " from Compute Engine metadata for the default service account: %s",
statusCode, response.parseAsString()));
return super.getProjectId();
}
return response.parseAsString();
} catch (IOException e) {
LoggingUtils.log(
LOGGER_PROVIDER,
Level.WARNING,
Collections.emptyMap(),
String.format(
"Unexpected Error: %s trying to get project ID"
+ " from Compute Engine metadata server. Reason: %s",
e.getMessage(), e.getCause().toString()));
return super.getProjectId();
}
}

/** Refresh the access token by getting it from the GCE metadata server */
@Override
public AccessToken refreshAccessToken() throws IOException {
Expand Down Expand Up @@ -564,6 +635,11 @@ static boolean checkStaticGceDetection(DefaultCredentialsProvider provider) {
return false;
}

@VisibleForTesting
void setProjectId(String projectId) {
this.projectId = projectId;
}

private static boolean pingComputeEngineMetadata(
HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
GenericUrl tokenUrl = new GenericUrl(getMetadataServerUrl(provider));
Expand Down Expand Up @@ -642,6 +718,11 @@ public static String getIdentityDocumentUrl() {
+ "/computeMetadata/v1/instance/service-accounts/default/identity";
}

public static String getProjectIdUrl() {
return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
+ "/computeMetadata/v1/project/project-id";
}

@Override
public int hashCode() {
return Objects.hash(transportFactoryClassName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,60 @@ void idTokenWithAudience_503StatusCode() {
GoogleAuthException.class, () -> credentials.idTokenWithAudience("Audience", null));
}

@Test
void getProjectId_metadataServerSuccess() {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport =
new MockMetadataServerTransport() {
@Override
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
return new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
return new MockLowLevelHttpResponse()
.setStatusCode(HttpStatusCodes.STATUS_CODE_OK)
.setContent("some-project-id");
}
};
}
};

ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
String projectId = credentials.getProjectId();
assertEquals("some-project-id", projectId);
}

@Test
void getProjectId_metadataServerFailure_404StatusCode() {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
assertNull(credentials.getProjectId());
}

@Test
void getProjectId_metadataServerFailure_otherStatusCode() {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
assertNull(credentials.getProjectId());
}

@Test
void getProjectId_explicitSet_noMDsCall() {
MockRequestCountingTransportFactory transportFactory =
new MockRequestCountingTransportFactory();
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
credentials.setProjectId("explicit.project_id");

assertEquals("explicit.project_id", credentials.getProjectId());
assertEquals(0, transportFactory.transport.getRequestCount());
}

static class MockMetadataServerTransportFactory implements HttpTransportFactory {

MockMetadataServerTransport transport =
Expand Down
Loading