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 @@ -800,8 +800,20 @@ public String getAccount() {
@InternalApi
@Override
public String getRegionalAccessBoundaryUrl() throws IOException {
String account = getAccount();
// In GKE environments, the default service account might return a non-email placeholder.
// Since RAB lookup requires a valid email-based service account, we skip RAB lookup
// in non-email scenarios by returning null.
if (account == null || !account.contains("@")) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know the document may have changed since this PR was raised, but I think the the standard email pattern is a regex and we will need to confirm to that.

LoggingUtils.log(
LOGGER_PROVIDER,
Level.INFO,
Collections.emptyMap(),
"Regional Access Boundary lookup is skipped for this instance because it is a non-email instance.");
return null;
}
Comment on lines +807 to +809
Copy link
Copy Markdown
Member

@lqiu96 lqiu96 Jun 3, 2026

Choose a reason for hiding this comment

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

I think we will also need to follow the recommendation for a one-time log message that the RAB instance is skipped (maybe here, maybe in the RABManager)

return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, getAccount());
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, account);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
Expand Down Expand Up @@ -85,6 +86,8 @@ final class RegionalAccessBoundaryManager {
private final AtomicReference<CooldownState> cooldownState =
new AtomicReference<>(new CooldownState(0, INITIAL_COOLDOWN_MILLIS));

private final AtomicBoolean skipRAB = new AtomicBoolean(false);

// Unbounded thread creation is discouraged in library code to avoid resource
// exhaustion. A shared, bounded executor service ensures a hard limit (5)
// on concurrent refresh tasks, while threadCount provides unique names
Expand Down Expand Up @@ -178,7 +181,7 @@ void triggerAsyncRefresh(
final HttpTransportFactory transportFactory,
final RegionalAccessBoundaryProvider provider,
final AccessToken accessToken) {
if (isCooldownActive()) {
if (skipRAB.get() || isCooldownActive()) {
return;
}

Expand All @@ -196,6 +199,11 @@ void triggerAsyncRefresh(
() -> {
try {
String url = provider.getRegionalAccessBoundaryUrl();
if (url == null) {
skipRAB.set(true);
future.set(null);
return;
}
Comment on lines +199 to +202
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq, can you remind me again why we need to do future.set(null);?

RegionalAccessBoundary newRAB =
RegionalAccessBoundary.refresh(
transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis);
Expand Down Expand Up @@ -279,6 +287,11 @@ long getCurrentCooldownMillis() {
return cooldownState.get().durationMillis;
}

@VisibleForTesting
boolean isSkipRAB() {
return skipRAB.get();
}

private static class CooldownState {
/** The time (in milliseconds from epoch) when the current cooldown period expires. */
final long expiryTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,48 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce
Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}

@org.junit.jupiter.api.Test
void refresh_regionalAccessBoundaryNonEmail_skipsRABLookup()
throws IOException, InterruptedException {
String nonEmailAccount = "non-email-account-value";
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
RegionalAccessBoundary regionalAccessBoundary =
new RegionalAccessBoundary(
TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS,
null);
transportFactory.transport.setRegionalAccessBoundary(regionalAccessBoundary);
transportFactory.transport.setServiceAccountEmail(nonEmailAccount);

ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();

// Before any call, skipRAB flag should be false
assertFalse(credentials.regionalAccessBoundaryManager.isSkipRAB());

// First call: triggers lookup which determines non-email, returns null, and sets skipRAB to
// true
Map<String, List<String>> headers = credentials.getRequestMetadata();
assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));

// Since the task is scheduled asynchronously on the shared executor, wait for it to complete
long deadline = System.currentTimeMillis() + 5000;
while (!credentials.regionalAccessBoundaryManager.isSkipRAB()
&& System.currentTimeMillis() < deadline) {
Thread.sleep(50);
}

// Verify skipRAB flag has been set to true
assertTrue(credentials.regionalAccessBoundaryManager.isSkipRAB());

// Verify RAB is still null
assertNull(credentials.getRegionalAccessBoundary());

// Second call: should bypass triggerAsyncRefresh completely and remain null
headers = credentials.getRequestMetadata();
assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
}

private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
throws InterruptedException {
long deadline = System.currentTimeMillis() + 5000;
Expand Down
Loading