Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.io.Closeable;

/**
* {@code ChannelCredentials} which holds allocated resources (e.g. file watchers) upon
* instantiation of a given {@code ChannelCredentials} object, which must be closed once
* mentioned {@code ChannelCredentials} are no longer in use.
*/
public final class ResourceAllocatingChannelCredentials extends ChannelCredentials {
Copy link
Member

Choose a reason for hiding this comment

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

Put this in XDS. We don't want a new public type to workaround our internals.

public static ChannelCredentials create(
ChannelCredentials channelCreds, ImmutableList<Closeable> resources) {
return new ResourceAllocatingChannelCredentials(channelCreds, resources);
}

private final ChannelCredentials channelCreds;
private final ImmutableList<Closeable> resources;

private ResourceAllocatingChannelCredentials(
ChannelCredentials channelCreds, ImmutableList<Closeable> resources) {
this.channelCreds = Preconditions.checkNotNull(channelCreds, "channelCreds");
this.resources = Preconditions.checkNotNull(resources, "resources");
}

public ChannelCredentials getChannelCredentials() {
return channelCreds;
}

public ImmutableList<Closeable> getAllocatedResources() {
return resources;
}

@Override
public ChannelCredentials withoutBearerTokens() {
return channelCreds.withoutBearerTokens();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import java.io.Closeable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link ResourceAllocatingChannelCredentials}. */
@RunWith(JUnit4.class)
public class ResourceAllocatingChannelCredentialsTest {
@Test
public void withoutBearerTokenDelegatesCall() {
ChannelCredentials channelChreds = new ChannelCredentials() {
@Override
public ChannelCredentials withoutBearerTokens() {
return this;
}
};
ImmutableList<Closeable> resources = ImmutableList.<Closeable>of();
ChannelCredentials creds =
ResourceAllocatingChannelCredentials.create(channelChreds, resources);
assertThat(creds.withoutBearerTokens()).isEqualTo(channelChreds);
}
}
34 changes: 15 additions & 19 deletions xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import io.grpc.ChannelCredentials;
import com.google.common.collect.ImmutableSet;
import io.grpc.internal.JsonUtil;
import io.grpc.xds.client.BootstrapperImpl;
import io.grpc.xds.client.XdsInitializationException;
Expand Down Expand Up @@ -90,47 +90,43 @@ protected String getJsonContent() throws XdsInitializationException, IOException
}

@Override
protected Object getImplSpecificConfig(Map<String, ?> serverConfig, String serverUri)
protected ImmutableMap<String, ?> getImplSpecificConfig(Map<String, ?> serverConfig,
Copy link
Member

Choose a reason for hiding this comment

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

All of this plumbing change should go back to the old way. We need to parse early so that it is actually validated. Instead, we communicate through the ChannelCredentials. (I'll make a comment elsewhere.)

String serverUri)
throws XdsInitializationException {
return getChannelCredentials(serverConfig, serverUri);
return getChannelCredentialsConfig(serverConfig, serverUri);
}

private static ChannelCredentials getChannelCredentials(Map<String, ?> serverConfig,
String serverUri)
private static ImmutableMap<String, ?> getChannelCredentialsConfig(Map<String, ?> serverConfig,
String serverUri)
throws XdsInitializationException {
List<?> rawChannelCredsList = JsonUtil.getList(serverConfig, "channel_creds");
if (rawChannelCredsList == null || rawChannelCredsList.isEmpty()) {
throw new XdsInitializationException(
"Invalid bootstrap: server " + serverUri + " 'channel_creds' required");
}
ChannelCredentials channelCredentials =
ImmutableMap<String, ?> channelCredentialsConfig =
parseChannelCredentials(JsonUtil.checkObjectList(rawChannelCredsList), serverUri);
if (channelCredentials == null) {
if (channelCredentialsConfig == null) {
throw new XdsInitializationException(
"Server " + serverUri + ": no supported channel credentials found");
}
return channelCredentials;
return channelCredentialsConfig;
}

@Nullable
private static ChannelCredentials parseChannelCredentials(List<Map<String, ?>> jsonList,
String serverUri)
private static ImmutableMap<String, ?> parseChannelCredentials(List<Map<String, ?>> jsonList,
String serverUri)
throws XdsInitializationException {
for (Map<String, ?> channelCreds : jsonList) {
String type = JsonUtil.getString(channelCreds, "type");
if (type == null) {
throw new XdsInitializationException(
"Invalid bootstrap: server " + serverUri + " with 'channel_creds' type unspecified");
}
XdsCredentialsProvider provider = XdsCredentialsRegistry.getDefaultRegistry()
.getProvider(type);
if (provider != null) {
Map<String, ?> config = JsonUtil.getObject(channelCreds, "config");
if (config == null) {
config = ImmutableMap.of();
}

return provider.newChannelCredentials(config);
ImmutableSet<String> supportedNames = XdsCredentialsRegistry.getDefaultRegistry()
.getSupportedCredentialNames();
if (supportedNames.contains(type)) {
return ImmutableMap.copyOf(channelCreds);
}
}
return null;
Expand Down
39 changes: 35 additions & 4 deletions xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.grpc.CallCredentials;
import io.grpc.CallOptions;
import io.grpc.ChannelCredentials;
Expand All @@ -28,9 +30,15 @@
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.ResourceAllocatingChannelCredentials;
import io.grpc.Status;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.JsonUtil;
import io.grpc.xds.client.Bootstrapper;
import io.grpc.xds.client.XdsInitializationException;
import io.grpc.xds.client.XdsTransportFactory;
import java.io.Closeable;
import java.util.Map;
import java.util.concurrent.TimeUnit;

final class GrpcXdsTransportFactory implements XdsTransportFactory {
Expand All @@ -42,7 +50,7 @@ final class GrpcXdsTransportFactory implements XdsTransportFactory {
}

@Override
public XdsTransport create(Bootstrapper.ServerInfo serverInfo) {
public XdsTransport create(Bootstrapper.ServerInfo serverInfo) throws XdsInitializationException {
return new GrpcXdsTransport(serverInfo, callCredentials);
}

Expand All @@ -56,8 +64,9 @@ static class GrpcXdsTransport implements XdsTransport {

private final ManagedChannel channel;
private final CallCredentials callCredentials;
private final ImmutableList<Closeable> resources;

public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo) {
public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo) throws XdsInitializationException {
this(serverInfo, null);
}

Expand All @@ -66,9 +75,27 @@ public GrpcXdsTransport(ManagedChannel channel) {
this(channel, null);
}

public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials) {
public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials)
throws XdsInitializationException {
String target = serverInfo.target();
ChannelCredentials channelCredentials = (ChannelCredentials) serverInfo.implSpecificConfig();
Map<String, ?> implSpecificConfig = serverInfo.implSpecificConfig();
String type = JsonUtil.getString(implSpecificConfig, "type");
XdsCredentialsProvider provider = XdsCredentialsRegistry.getDefaultRegistry()
.getProvider(type);
Map<String, ?> config = JsonUtil.getObject(implSpecificConfig, "config");
if (config == null) {
config = ImmutableMap.of();
}
ChannelCredentials channelCredentials = provider.newChannelCredentials(config);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of creating the ChannelCredentials now, which is necessary because your ResourceAllocatingChannelCredentials API can only release the resources once, we want to share the credentials and keep track of how many usages there are.

public final class ResourceAllocatingChannelCredentials extends ChannelCredentials {
  public static ChannelCredentials create(
      ChannelCredentials channelCreds, Supplier<Closeable> resource) {
    return new ResourceAllocatingChannelCredentials(channelCreds, resource);
  }

  private final ChannelCredentials channelCreds;
  private final Closeable resourceReleaser;
  private final int refCount;

  // constructor...

  public synchronized ChannelCredentials acquireChannelCredentials() {
    if (refCount++ == 0) {
      this.resourceReleaser = resource.get();
    }
    return channelCreds;
  }

  public synchronized releaseChannelCredentials() {
    if (--refCount == 0) {
      resourceReleaser.close();
      resourceReleaser = null;
    }
    assert refCount >= 0;
  }

  @Override
  public ChannelCredentials withoutBearerTokens() {
    // Could be implemented, but unnecessary. Would need to return a ResourceAllocatingChannelCredentials or similar class
    throw new UnsupportedOperationException();
  }
}

Unfortunately, the error handling in resource.get() gets ugly; AdvancedTlsx509TrustManager.updateTrustCredentials() throws. To limp along, we should probably let that throw, and if it throws we return a FailingXdsTransport here.

That's not really what we want, as we don't want a transient failure to become permanent, but it seems really hard to "correctly," so I think we should accept something lesser for the while.

if (channelCredentials == null) {
throw new XdsInitializationException(
"Cannot create channel credentials of type " + type + " for target " + target);
}
// if {@code ChannelCredentials} instance has allocated resource of any type, save them to be
// released once the channel is shutdown
this.resources = (channelCredentials instanceof ResourceAllocatingChannelCredentials)
? ((ResourceAllocatingChannelCredentials) channelCredentials).getAllocatedResources()
: ImmutableList.<Closeable>of();
this.channel = Grpc.newChannelBuilder(target, channelCredentials)
.keepAliveTime(5, TimeUnit.MINUTES)
.build();
Expand All @@ -79,6 +106,7 @@ public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials call
public GrpcXdsTransport(ManagedChannel channel, CallCredentials callCredentials) {
this.channel = checkNotNull(channel, "channel");
this.callCredentials = callCredentials;
this.resources = ImmutableList.<Closeable>of();
}

@Override
Expand All @@ -99,6 +127,9 @@ public <ReqT, RespT> StreamingCall<ReqT, RespT> createStreamingCall(
@Override
public void shutdown() {
channel.shutdown();
for (Closeable resource : resources) {
GrpcUtil.closeQuietly(resource);
}
}

private class XdsStreamingCall<ReqT, RespT> implements
Expand Down
9 changes: 9 additions & 0 deletions xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.InternalServiceProviders;
import java.util.ArrayList;
Expand Down Expand Up @@ -147,6 +148,14 @@ public synchronized XdsCredentialsProvider getProvider(String name) {
return effectiveProviders.get(checkNotNull(name, "name"));
}

/**
* Returns list of registered xds credential names. Each provider declares its name via
* {@link XdsCredentialsProvider#getName}.
*/
public synchronized ImmutableSet<String> getSupportedCredentialNames() {
return effectiveProviders.keySet();
}

@VisibleForTesting
static List<Class<?>> getHardCodedClasses() {
// Class.forName(String) is used to remove the need for ProGuard configuration. Note that
Expand Down
7 changes: 4 additions & 3 deletions xds/src/main/java/io/grpc/xds/client/Bootstrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public BootstrapInfo bootstrap(Map<String, ?> rawData) throws XdsInitializationE
public abstract static class ServerInfo {
public abstract String target();

public abstract Object implSpecificConfig();
public abstract ImmutableMap<String, ?> implSpecificConfig();

public abstract boolean ignoreResourceDeletion();

Expand All @@ -66,14 +66,15 @@ public abstract static class ServerInfo {
public abstract boolean resourceTimerIsTransientError();

@VisibleForTesting
public static ServerInfo create(String target, @Nullable Object implSpecificConfig) {
public static ServerInfo create(
String target, @Nullable ImmutableMap<String, ?> implSpecificConfig) {
return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig,
false, false, false);
}

@VisibleForTesting
public static ServerInfo create(
String target, Object implSpecificConfig, boolean ignoreResourceDeletion,
String target, ImmutableMap<String, ?> implSpecificConfig, boolean ignoreResourceDeletion,
boolean isTrustedXdsServer, boolean resourceTimerIsTransientError) {
return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig,
ignoreResourceDeletion, isTrustedXdsServer, resourceTimerIsTransientError);
Expand Down
6 changes: 3 additions & 3 deletions xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ protected BootstrapperImpl() {

protected abstract String getJsonContent() throws IOException, XdsInitializationException;

protected abstract Object getImplSpecificConfig(Map<String, ?> serverConfig, String serverUri)
throws XdsInitializationException;
protected abstract ImmutableMap<String, ?> getImplSpecificConfig(
Map<String, ?> serverConfig, String serverUri) throws XdsInitializationException;


/**
Expand Down Expand Up @@ -253,7 +253,7 @@ private List<ServerInfo> parseServerInfos(List<?> rawServerConfigs, XdsLogger lo
}
logger.log(XdsLogLevel.INFO, "xDS server URI: {0}", serverUri);

Object implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri);
ImmutableMap<String, ?> implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri);

boolean resourceTimerIsTransientError = false;
boolean ignoreResourceDeletion = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10823")
public interface XdsTransportFactory {
XdsTransport create(Bootstrapper.ServerInfo serverInfo);
XdsTransport create(Bootstrapper.ServerInfo serverInfo) throws XdsInitializationException;

/**
* Represents transport for xDS communication (e.g., a gRPC channel).
Expand Down
Loading
Loading