@@ -130,6 +130,7 @@ public class ComputeEngineCredentials extends GoogleCredentials
130130 private transient HttpTransportFactory transportFactory ;
131131
132132 private String universeDomainFromMetadata = null ;
133+ private String projectId = null ;
133134
134135 /**
135136 * Experimental Feature.
@@ -341,6 +342,72 @@ private String getUniverseDomainFromMetadata() throws IOException {
341342 return responseString ;
342343 }
343344
345+ /**
346+ * Retrieves the Google Cloud project ID from the Compute Engine (GCE) metadata server.
347+ * <p>
348+ * On its first successful execution, it fetches the project ID and caches it for the lifetime
349+ * of the object. Subsequent calls will return the cached value without making additional network
350+ * requests.
351+ * <p>
352+ * If the request to the metadata server fails (e.g., due to network issues, or if the VM lacks
353+ * the required service account permissions), the method will attempt to fall back to a default
354+ * project ID provider which could be {@code null}.
355+ *
356+ * @return the GCP project ID string, or {@code null} if the metadata server is
357+ * inaccessible and no fallback project ID can be determined.
358+ */
359+ @ Override
360+ public String getProjectId () {
361+ synchronized (this ) {
362+ if (this .projectId != null ) {
363+ return this .projectId ;
364+ }
365+ }
366+
367+ String projectIdFromMetadata = getProjectIdFromMetadata ();
368+ synchronized (this ) {
369+ this .projectId = projectIdFromMetadata ;
370+ }
371+ return projectIdFromMetadata ;
372+ }
373+
374+ private String getProjectIdFromMetadata () {
375+ try {
376+ HttpResponse response = getMetadataResponse (getProjectIdUrl (), RequestType .UNTRACKED , false );
377+ int statusCode = response .getStatusCode ();
378+ if (statusCode == HttpStatusCodes .STATUS_CODE_NOT_FOUND ) {
379+ LoggingUtils .log (LOGGER_PROVIDER , Level .WARNING , Collections .emptyMap (), String .format (
380+ "Error code %s trying to get project ID from"
381+ + " Compute Engine metadata. This may be because the virtual machine instance"
382+ + " does not have permission scopes specified." ,
383+ statusCode ));
384+ return super .getProjectId ();
385+ }
386+ if (statusCode != HttpStatusCodes .STATUS_CODE_OK ) {
387+ LoggingUtils .log (
388+ LOGGER_PROVIDER ,
389+ Level .WARNING ,
390+ Collections .emptyMap (),
391+ String .format (
392+ "Unexpected Error code %s trying to get project ID"
393+ + " from Compute Engine metadata for the default service account: %s" ,
394+ statusCode , response .parseAsString ()));
395+ return super .getProjectId ();
396+ }
397+ return response .parseAsString ();
398+ } catch (IOException e ) {
399+ LoggingUtils .log (
400+ LOGGER_PROVIDER ,
401+ Level .WARNING ,
402+ Collections .emptyMap (),
403+ String .format (
404+ "Unexpected Error: %s trying to get project ID"
405+ + " from Compute Engine metadata server. Reason: %s" ,
406+ e .getMessage (), e .getCause ().toString ()));
407+ return super .getProjectId ();
408+ }
409+ }
410+
344411 /** Refresh the access token by getting it from the GCE metadata server */
345412 @ Override
346413 public AccessToken refreshAccessToken () throws IOException {
@@ -642,6 +709,11 @@ public static String getIdentityDocumentUrl() {
642709 + "/computeMetadata/v1/instance/service-accounts/default/identity" ;
643710 }
644711
712+ public static String getProjectIdUrl () {
713+ return getMetadataServerUrl (DefaultCredentialsProvider .DEFAULT )
714+ + "/computeMetadata/v1/project/project-id" ;
715+ }
716+
645717 @ Override
646718 public int hashCode () {
647719 return Objects .hash (transportFactoryClassName );
0 commit comments