• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.base.Preconditions.checkNotNull;
20 
21 import com.google.common.annotations.VisibleForTesting;
22 import com.google.common.base.Charsets;
23 import com.google.common.base.Preconditions;
24 import com.google.common.base.Strings;
25 import com.google.common.collect.ImmutableList;
26 import com.google.common.collect.ImmutableMap;
27 import com.google.common.io.CharStreams;
28 import io.grpc.NameResolver;
29 import io.grpc.NameResolverRegistry;
30 import io.grpc.Status;
31 import io.grpc.SynchronizationContext;
32 import io.grpc.alts.InternalCheckGcpEnvironment;
33 import io.grpc.internal.GrpcUtil;
34 import io.grpc.internal.SharedResourceHolder;
35 import io.grpc.internal.SharedResourceHolder.Resource;
36 import java.io.IOException;
37 import java.io.InputStreamReader;
38 import java.io.Reader;
39 import java.net.HttpURLConnection;
40 import java.net.URI;
41 import java.net.URISyntaxException;
42 import java.net.URL;
43 import java.util.Map;
44 import java.util.Random;
45 import java.util.concurrent.Executor;
46 
47 /**
48  * CloudToProd version of {@link NameResolver}.
49  */
50 final class GoogleCloudToProdNameResolver extends NameResolver {
51 
52   @VisibleForTesting
53   static final String METADATA_URL_ZONE =
54       "http://metadata.google.internal/computeMetadata/v1/instance/zone";
55   @VisibleForTesting
56   static final String METADATA_URL_SUPPORT_IPV6 =
57       "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s";
58   static final String C2P_AUTHORITY = "traffic-director-c2p.xds.googleapis.com";
59   @VisibleForTesting
60   static boolean isOnGcp = InternalCheckGcpEnvironment.isOnGcp();
61   @VisibleForTesting
62   static boolean xdsBootstrapProvided =
63       System.getenv("GRPC_XDS_BOOTSTRAP") != null
64           || System.getProperty("io.grpc.xds.bootstrap") != null
65           || System.getenv("GRPC_XDS_BOOTSTRAP_CONFIG") != null
66           || System.getProperty("io.grpc.xds.bootstrapConfig") != null;
67   @VisibleForTesting
68   static boolean enableFederation =
69       Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION"))
70           || Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION"));
71 
72   private static final String serverUriOverride =
73       System.getenv("GRPC_TEST_ONLY_GOOGLE_C2P_RESOLVER_TRAFFIC_DIRECTOR_URI");
74 
75   private HttpConnectionProvider httpConnectionProvider = HttpConnectionFactory.INSTANCE;
76   private final String authority;
77   private final SynchronizationContext syncContext;
78   private final Resource<Executor> executorResource;
79   private final BootstrapSetter bootstrapSetter;
80   private final NameResolver delegate;
81   private final Random rand;
82   private final boolean usingExecutorResource;
83   // It's not possible to use both PSM and DirectPath C2P in the same application.
84   // Delegate to DNS if user-provided bootstrap is found.
85   private final String schemeOverride =
86       !isOnGcp
87       || (xdsBootstrapProvided && !enableFederation)
88       ? "dns" : "xds";
89   private Executor executor;
90   private Listener2 listener;
91   private boolean succeeded;
92   private boolean resolving;
93   private boolean shutdown;
94 
GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource, BootstrapSetter bootstrapSetter)95   GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource,
96       BootstrapSetter bootstrapSetter) {
97     this(targetUri, args, executorResource, new Random(), bootstrapSetter,
98         NameResolverRegistry.getDefaultRegistry().asFactory());
99   }
100 
101   @VisibleForTesting
GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource, Random rand, BootstrapSetter bootstrapSetter, NameResolver.Factory nameResolverFactory)102   GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource,
103       Random rand, BootstrapSetter bootstrapSetter, NameResolver.Factory nameResolverFactory) {
104     this.executorResource = checkNotNull(executorResource, "executorResource");
105     this.bootstrapSetter = checkNotNull(bootstrapSetter, "bootstrapSetter");
106     this.rand = checkNotNull(rand, "rand");
107     String targetPath = checkNotNull(checkNotNull(targetUri, "targetUri").getPath(), "targetPath");
108     Preconditions.checkArgument(
109         targetPath.startsWith("/"),
110         "the path component (%s) of the target (%s) must start with '/'",
111         targetPath,
112         targetUri);
113     authority = GrpcUtil.checkAuthority(targetPath.substring(1));
114     syncContext = checkNotNull(args, "args").getSynchronizationContext();
115     targetUri = overrideUriScheme(targetUri, schemeOverride);
116     if (schemeOverride.equals("xds") && enableFederation) {
117       targetUri = overrideUriAuthority(targetUri, C2P_AUTHORITY);
118     }
119     delegate = checkNotNull(nameResolverFactory, "nameResolverFactory").newNameResolver(
120         targetUri, args);
121     executor = args.getOffloadExecutor();
122     usingExecutorResource = executor == null;
123   }
124 
125   @Override
getServiceAuthority()126   public String getServiceAuthority() {
127     return authority;
128   }
129 
130   @Override
start(final Listener2 listener)131   public void start(final Listener2 listener) {
132     if (delegate == null) {
133       listener.onError(Status.INTERNAL.withDescription(
134           "Delegate resolver not found, scheme: " + schemeOverride));
135       return;
136     }
137     this.listener = checkNotNull(listener, "listener");
138     resolve();
139   }
140 
resolve()141   private void resolve() {
142     if (resolving || shutdown || delegate == null) {
143       return;
144     }
145     resolving = true;
146     if (schemeOverride.equals("dns")) {
147       delegate.start(listener);
148       succeeded = true;
149       resolving = false;
150       return;
151     }
152     if (executor == null) {
153       executor = SharedResourceHolder.get(executorResource);
154     }
155 
156     class Resolve implements Runnable {
157       @Override
158       public void run() {
159         ImmutableMap<String, ?> rawBootstrap = null;
160         try {
161           // User provided bootstrap configs are only supported with federation. If federation is
162           // not enabled or there is no user provided config, we set a custom bootstrap override.
163           // Otherwise, we don't set the override, which will allow a user provided bootstrap config
164           // to take effect.
165           if (!enableFederation || !xdsBootstrapProvided) {
166             rawBootstrap = generateBootstrap(queryZoneMetadata(METADATA_URL_ZONE),
167                 queryIpv6SupportMetadata(METADATA_URL_SUPPORT_IPV6));
168           }
169         } catch (IOException e) {
170           listener.onError(
171               Status.INTERNAL.withDescription("Unable to get metadata").withCause(e));
172         } finally {
173           final ImmutableMap<String, ?> finalRawBootstrap = rawBootstrap;
174           syncContext.execute(new Runnable() {
175             @Override
176             public void run() {
177               if (!shutdown) {
178                 if (finalRawBootstrap != null) {
179                   bootstrapSetter.setBootstrap(finalRawBootstrap);
180                 }
181                 delegate.start(listener);
182                 succeeded = true;
183               }
184               resolving = false;
185             }
186           });
187         }
188       }
189     }
190 
191     executor.execute(new Resolve());
192   }
193 
generateBootstrap(String zone, boolean supportIpv6)194   private ImmutableMap<String, ?> generateBootstrap(String zone, boolean supportIpv6) {
195     ImmutableMap.Builder<String, Object> nodeBuilder = ImmutableMap.builder();
196     nodeBuilder.put("id", "C2P-" + (rand.nextInt() & Integer.MAX_VALUE));
197     if (!zone.isEmpty()) {
198       nodeBuilder.put("locality", ImmutableMap.of("zone", zone));
199     }
200     if (supportIpv6) {
201       nodeBuilder.put("metadata",
202           ImmutableMap.of("TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE", true));
203     }
204     ImmutableMap.Builder<String, Object> serverBuilder = ImmutableMap.builder();
205     String serverUri = "directpath-pa.googleapis.com";
206     if (serverUriOverride != null && serverUriOverride.length() > 0) {
207       serverUri = serverUriOverride;
208     }
209     serverBuilder.put("server_uri", serverUri);
210     serverBuilder.put("channel_creds",
211         ImmutableList.of(ImmutableMap.of("type", "google_default")));
212     serverBuilder.put("server_features", ImmutableList.of("xds_v3", "ignore_resource_deletion"));
213     ImmutableMap.Builder<String, Object> authoritiesBuilder = ImmutableMap.builder();
214     authoritiesBuilder.put(
215         C2P_AUTHORITY,
216         ImmutableMap.of("xds_servers", ImmutableList.of(serverBuilder.buildOrThrow())));
217     return ImmutableMap.of(
218         "node", nodeBuilder.buildOrThrow(),
219         "xds_servers", ImmutableList.of(serverBuilder.buildOrThrow()),
220         "authorities", authoritiesBuilder.buildOrThrow());
221   }
222 
223   @Override
refresh()224   public void refresh() {
225     if (succeeded) {
226       delegate.refresh();
227     } else if (!resolving) {
228       resolve();
229     }
230   }
231 
232   @Override
shutdown()233   public void shutdown() {
234     if (shutdown) {
235       return;
236     }
237     shutdown = true;
238     if (delegate != null) {
239       delegate.shutdown();
240     }
241     if (executor != null && usingExecutorResource) {
242       executor = SharedResourceHolder.release(executorResource, executor);
243     }
244   }
245 
queryZoneMetadata(String url)246   private String queryZoneMetadata(String url) throws IOException {
247     HttpURLConnection con = null;
248     String respBody;
249     try {
250       con = httpConnectionProvider.createConnection(url);
251       if (con.getResponseCode() != 200) {
252         return "";
253       }
254       try (Reader reader = new InputStreamReader(con.getInputStream(), Charsets.UTF_8)) {
255         respBody = CharStreams.toString(reader);
256       }
257     } finally {
258       if (con != null) {
259         con.disconnect();
260       }
261     }
262     int index = respBody.lastIndexOf('/');
263     return index == -1 ? "" : respBody.substring(index + 1);
264   }
265 
queryIpv6SupportMetadata(String url)266   private boolean queryIpv6SupportMetadata(String url) throws IOException {
267     HttpURLConnection con = null;
268     try {
269       con = httpConnectionProvider.createConnection(url);
270       return con.getResponseCode() == 200;
271     } finally {
272       if (con != null) {
273         con.disconnect();
274       }
275     }
276   }
277 
278   @VisibleForTesting
setHttpConnectionProvider(HttpConnectionProvider httpConnectionProvider)279   void setHttpConnectionProvider(HttpConnectionProvider httpConnectionProvider) {
280     this.httpConnectionProvider = httpConnectionProvider;
281   }
282 
overrideUriScheme(URI uri, String scheme)283   private static URI overrideUriScheme(URI uri, String scheme) {
284     URI res;
285     try {
286       res = new URI(scheme, uri.getAuthority(), uri.getPath(), uri.getQuery(), uri.getFragment());
287     } catch (URISyntaxException ex) {
288       throw new IllegalArgumentException("Invalid scheme: " + scheme, ex);
289     }
290     return res;
291   }
292 
overrideUriAuthority(URI uri, String authority)293   private static URI overrideUriAuthority(URI uri, String authority) {
294     URI res;
295     try {
296       res = new URI(uri.getScheme(), authority, uri.getPath(), uri.getQuery(), uri.getFragment());
297     } catch (URISyntaxException ex) {
298       throw new IllegalArgumentException("Invalid authority: " + authority, ex);
299     }
300     return res;
301   }
302 
303   private enum HttpConnectionFactory implements HttpConnectionProvider {
304     INSTANCE;
305 
306     @Override
createConnection(String url)307     public HttpURLConnection createConnection(String url) throws IOException {
308       HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
309       con.setRequestMethod("GET");
310       con.setReadTimeout(10000);
311       con.setRequestProperty("Metadata-Flavor", "Google");
312       return con;
313     }
314   }
315 
316   @VisibleForTesting
317   interface HttpConnectionProvider {
createConnection(String url)318     HttpURLConnection createConnection(String url) throws IOException;
319   }
320 
321   public interface BootstrapSetter {
setBootstrap(Map<String, ?> bootstrap)322     void setBootstrap(Map<String, ?> bootstrap);
323   }
324 }
325