1 /* 2 * Copyright 2021 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.googleapis; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.mockito.Mockito.mock; 21 import static org.mockito.Mockito.verify; 22 import static org.mockito.Mockito.when; 23 24 import com.google.common.collect.ImmutableList; 25 import com.google.common.collect.ImmutableMap; 26 import com.google.common.collect.Iterables; 27 import io.grpc.ChannelLogger; 28 import io.grpc.NameResolver; 29 import io.grpc.NameResolver.Args; 30 import io.grpc.NameResolver.ServiceConfigParser; 31 import io.grpc.NameResolverProvider; 32 import io.grpc.NameResolverRegistry; 33 import io.grpc.Status; 34 import io.grpc.Status.Code; 35 import io.grpc.SynchronizationContext; 36 import io.grpc.googleapis.GoogleCloudToProdNameResolver.HttpConnectionProvider; 37 import io.grpc.internal.FakeClock; 38 import io.grpc.internal.GrpcUtil; 39 import io.grpc.internal.SharedResourceHolder.Resource; 40 import java.io.ByteArrayInputStream; 41 import java.io.IOException; 42 import java.net.HttpURLConnection; 43 import java.net.URI; 44 import java.nio.charset.StandardCharsets; 45 import java.util.HashMap; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Random; 49 import java.util.concurrent.Executor; 50 import java.util.concurrent.atomic.AtomicReference; 51 import org.junit.After; 52 import org.junit.Before; 53 import org.junit.Rule; 54 import org.junit.Test; 55 import org.junit.runner.RunWith; 56 import org.junit.runners.JUnit4; 57 import org.mockito.ArgumentCaptor; 58 import org.mockito.Captor; 59 import org.mockito.Mock; 60 import org.mockito.junit.MockitoJUnit; 61 import org.mockito.junit.MockitoRule; 62 63 @RunWith(JUnit4.class) 64 public class GoogleCloudToProdNameResolverTest { 65 66 @Rule 67 public final MockitoRule mocks = MockitoJUnit.rule(); 68 69 private static final URI TARGET_URI = URI.create("google-c2p:///googleapis.com"); 70 private static final String ZONE = "us-central1-a"; 71 private static final int DEFAULT_PORT = 887; 72 73 private final SynchronizationContext syncContext = new SynchronizationContext( 74 new Thread.UncaughtExceptionHandler() { 75 @Override 76 public void uncaughtException(Thread t, Throwable e) { 77 throw new AssertionError(e); 78 } 79 }); 80 private final NameResolver.Args args = NameResolver.Args.newBuilder() 81 .setDefaultPort(DEFAULT_PORT) 82 .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR) 83 .setSynchronizationContext(syncContext) 84 .setServiceConfigParser(mock(ServiceConfigParser.class)) 85 .setChannelLogger(mock(ChannelLogger.class)) 86 .build(); 87 private final FakeClock fakeExecutor = new FakeClock(); 88 private final FakeBootstrapSetter fakeBootstrapSetter = new FakeBootstrapSetter(); 89 private final Resource<Executor> fakeExecutorResource = new Resource<Executor>() { 90 @Override 91 public Executor create() { 92 return fakeExecutor.getScheduledExecutorService(); 93 } 94 95 @Override 96 public void close(Executor instance) {} 97 }; 98 99 private final NameResolverRegistry nsRegistry = new NameResolverRegistry(); 100 private final Map<String, NameResolver> delegatedResolver = new HashMap<>(); 101 102 @Mock 103 private NameResolver.Listener2 mockListener; 104 private Random random = new Random(1); 105 @Captor 106 private ArgumentCaptor<Status> errorCaptor; 107 private boolean originalIsOnGcp; 108 private boolean originalXdsBootstrapProvided; 109 private GoogleCloudToProdNameResolver resolver; 110 111 @Before setUp()112 public void setUp() { 113 nsRegistry.register(new FakeNsProvider("dns")); 114 nsRegistry.register(new FakeNsProvider("xds")); 115 originalIsOnGcp = GoogleCloudToProdNameResolver.isOnGcp; 116 originalXdsBootstrapProvided = GoogleCloudToProdNameResolver.xdsBootstrapProvided; 117 } 118 119 @After tearDown()120 public void tearDown() { 121 GoogleCloudToProdNameResolver.isOnGcp = originalIsOnGcp; 122 GoogleCloudToProdNameResolver.xdsBootstrapProvided = originalXdsBootstrapProvided; 123 resolver.shutdown(); 124 verify(Iterables.getOnlyElement(delegatedResolver.values())).shutdown(); 125 } 126 createResolver()127 private void createResolver() { 128 HttpConnectionProvider httpConnections = new HttpConnectionProvider() { 129 @Override 130 public HttpURLConnection createConnection(String url) throws IOException { 131 HttpURLConnection con = mock(HttpURLConnection.class); 132 when(con.getResponseCode()).thenReturn(200); 133 if (url.equals(GoogleCloudToProdNameResolver.METADATA_URL_ZONE)) { 134 when(con.getInputStream()).thenReturn( 135 new ByteArrayInputStream(("/" + ZONE).getBytes(StandardCharsets.UTF_8))); 136 return con; 137 } else if (url.equals(GoogleCloudToProdNameResolver.METADATA_URL_SUPPORT_IPV6)) { 138 return con; 139 } 140 throw new AssertionError("Unknown http query"); 141 } 142 }; 143 resolver = new GoogleCloudToProdNameResolver( 144 TARGET_URI, args, fakeExecutorResource, random, fakeBootstrapSetter, 145 nsRegistry.asFactory()); 146 resolver.setHttpConnectionProvider(httpConnections); 147 } 148 149 @Test notOnGcpDelegateToDns()150 public void notOnGcpDelegateToDns() { 151 GoogleCloudToProdNameResolver.isOnGcp = false; 152 createResolver(); 153 resolver.start(mockListener); 154 assertThat(delegatedResolver.keySet()).containsExactly("dns"); 155 verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); 156 } 157 158 @Test hasProvidedBootstrapDelegateToDns()159 public void hasProvidedBootstrapDelegateToDns() { 160 GoogleCloudToProdNameResolver.isOnGcp = true; 161 GoogleCloudToProdNameResolver.xdsBootstrapProvided = true; 162 GoogleCloudToProdNameResolver.enableFederation = false; 163 createResolver(); 164 resolver.start(mockListener); 165 assertThat(delegatedResolver.keySet()).containsExactly("dns"); 166 verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); 167 } 168 169 @SuppressWarnings("unchecked") 170 @Test onGcpAndNoProvidedBootstrapDelegateToXds()171 public void onGcpAndNoProvidedBootstrapDelegateToXds() { 172 GoogleCloudToProdNameResolver.isOnGcp = true; 173 GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; 174 createResolver(); 175 resolver.start(mockListener); 176 fakeExecutor.runDueTasks(); 177 assertThat(delegatedResolver.keySet()).containsExactly("xds"); 178 verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); 179 Map<String, ?> bootstrap = fakeBootstrapSetter.bootstrapRef.get(); 180 Map<String, ?> node = (Map<String, ?>) bootstrap.get("node"); 181 assertThat(node).containsExactly( 182 "id", "C2P-991614323", 183 "locality", ImmutableMap.of("zone", ZONE), 184 "metadata", ImmutableMap.of("TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE", true)); 185 Map<String, ?> server = Iterables.getOnlyElement( 186 (List<Map<String, ?>>) bootstrap.get("xds_servers")); 187 assertThat(server).containsExactly( 188 "server_uri", "directpath-pa.googleapis.com", 189 "channel_creds", ImmutableList.of(ImmutableMap.of("type", "google_default")), 190 "server_features", ImmutableList.of("xds_v3", "ignore_resource_deletion")); 191 Map<String, ?> authorities = (Map<String, ?>) bootstrap.get("authorities"); 192 assertThat(authorities).containsExactly( 193 "traffic-director-c2p.xds.googleapis.com", 194 ImmutableMap.of("xds_servers", ImmutableList.of(server))); 195 } 196 197 @SuppressWarnings("unchecked") 198 @Test onGcpAndNoProvidedBootstrapAndFederationEnabledDelegateToXds()199 public void onGcpAndNoProvidedBootstrapAndFederationEnabledDelegateToXds() { 200 GoogleCloudToProdNameResolver.isOnGcp = true; 201 GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; 202 GoogleCloudToProdNameResolver.enableFederation = true; 203 createResolver(); 204 resolver.start(mockListener); 205 fakeExecutor.runDueTasks(); 206 assertThat(delegatedResolver.keySet()).containsExactly("xds"); 207 verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); 208 // check bootstrap 209 Map<String, ?> bootstrap = fakeBootstrapSetter.bootstrapRef.get(); 210 Map<String, ?> node = (Map<String, ?>) bootstrap.get("node"); 211 assertThat(node).containsExactly( 212 "id", "C2P-991614323", 213 "locality", ImmutableMap.of("zone", ZONE), 214 "metadata", ImmutableMap.of("TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE", true)); 215 Map<String, ?> server = Iterables.getOnlyElement( 216 (List<Map<String, ?>>) bootstrap.get("xds_servers")); 217 assertThat(server).containsExactly( 218 "server_uri", "directpath-pa.googleapis.com", 219 "channel_creds", ImmutableList.of(ImmutableMap.of("type", "google_default")), 220 "server_features", ImmutableList.of("xds_v3", "ignore_resource_deletion")); 221 Map<String, ?> authorities = (Map<String, ?>) bootstrap.get("authorities"); 222 assertThat(authorities).containsExactly( 223 "traffic-director-c2p.xds.googleapis.com", 224 ImmutableMap.of("xds_servers", ImmutableList.of(server))); 225 } 226 227 @SuppressWarnings("unchecked") 228 @Test onGcpAndProvidedBootstrapAndFederationEnabledDontDelegateToXds()229 public void onGcpAndProvidedBootstrapAndFederationEnabledDontDelegateToXds() { 230 GoogleCloudToProdNameResolver.isOnGcp = true; 231 GoogleCloudToProdNameResolver.xdsBootstrapProvided = true; 232 GoogleCloudToProdNameResolver.enableFederation = true; 233 createResolver(); 234 resolver.start(mockListener); 235 fakeExecutor.runDueTasks(); 236 assertThat(delegatedResolver.keySet()).containsExactly("xds"); 237 verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); 238 // Bootstrapper should not have been set, since there was no user provided config. 239 assertThat(fakeBootstrapSetter.bootstrapRef.get()).isNull(); 240 } 241 242 @Test failToQueryMetadata()243 public void failToQueryMetadata() { 244 GoogleCloudToProdNameResolver.isOnGcp = true; 245 GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; 246 createResolver(); 247 HttpConnectionProvider httpConnections = new HttpConnectionProvider() { 248 @Override 249 public HttpURLConnection createConnection(String url) throws IOException { 250 HttpURLConnection con = mock(HttpURLConnection.class); 251 when(con.getResponseCode()).thenThrow(new IOException("unknown error")); 252 return con; 253 } 254 }; 255 resolver.setHttpConnectionProvider(httpConnections); 256 resolver.start(mockListener); 257 fakeExecutor.runDueTasks(); 258 verify(mockListener).onError(errorCaptor.capture()); 259 assertThat(errorCaptor.getValue().getCode()).isEqualTo(Code.INTERNAL); 260 assertThat(errorCaptor.getValue().getDescription()).isEqualTo("Unable to get metadata"); 261 } 262 263 private final class FakeNsProvider extends NameResolverProvider { 264 private final String scheme; 265 FakeNsProvider(String scheme)266 private FakeNsProvider(String scheme) { 267 this.scheme = scheme; 268 } 269 270 @Override newNameResolver(URI targetUri, Args args)271 public NameResolver newNameResolver(URI targetUri, Args args) { 272 if (scheme.equals(targetUri.getScheme())) { 273 NameResolver resolver = mock(NameResolver.class); 274 delegatedResolver.put(scheme, resolver); 275 return resolver; 276 } 277 return null; 278 } 279 280 @Override isAvailable()281 protected boolean isAvailable() { 282 return true; 283 } 284 285 @Override priority()286 protected int priority() { 287 return 5; 288 } 289 290 @Override getDefaultScheme()291 public String getDefaultScheme() { 292 return scheme; 293 } 294 } 295 296 private static final class FakeBootstrapSetter 297 implements GoogleCloudToProdNameResolver.BootstrapSetter { 298 private final AtomicReference<Map<String, ?>> bootstrapRef = new AtomicReference<>(); 299 300 @Override setBootstrap(Map<String, ?> bootstrap)301 public void setBootstrap(Map<String, ?> bootstrap) { 302 bootstrapRef.set(bootstrap); 303 } 304 } 305 } 306