1 /* 2 * Copyright 2022 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.xds; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static io.grpc.xds.Bootstrapper.ServerInfo; 21 import static io.grpc.xds.XdsClient.ResourceUpdate; 22 import static io.grpc.xds.XdsClient.canonifyResourceName; 23 import static io.grpc.xds.XdsClient.isResourceNameValid; 24 import static io.grpc.xds.XdsClientImpl.ResourceInvalidException; 25 26 import com.google.common.annotations.VisibleForTesting; 27 import com.google.common.base.Strings; 28 import com.google.protobuf.Any; 29 import com.google.protobuf.InvalidProtocolBufferException; 30 import com.google.protobuf.Message; 31 import io.envoyproxy.envoy.service.discovery.v3.Resource; 32 import io.grpc.LoadBalancerRegistry; 33 import java.util.ArrayList; 34 import java.util.HashMap; 35 import java.util.HashSet; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 import javax.annotation.Nullable; 40 41 abstract class XdsResourceType<T extends ResourceUpdate> { 42 static final String TYPE_URL_RESOURCE = 43 "type.googleapis.com/envoy.service.discovery.v3.Resource"; 44 static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls"; 45 @VisibleForTesting 46 static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate"; 47 @VisibleForTesting 48 static final String HASH_POLICY_FILTER_STATE_KEY = "io.grpc.channel_id"; 49 @VisibleForTesting 50 static boolean enableRouteLookup = getFlag("GRPC_EXPERIMENTAL_XDS_RLS_LB", true); 51 @VisibleForTesting 52 static boolean enableLeastRequest = 53 !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) 54 ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) 55 : Boolean.parseBoolean(System.getProperty("io.grpc.xds.experimentalEnableLeastRequest")); 56 57 @VisibleForTesting 58 static boolean enableWrr = getFlag("GRPC_EXPERIMENTAL_XDS_WRR_LB", true); 59 60 @VisibleForTesting 61 static boolean enablePickFirst = getFlag("GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG", false); 62 63 static final String TYPE_URL_CLUSTER_CONFIG = 64 "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig"; 65 static final String TYPE_URL_TYPED_STRUCT_UDPA = 66 "type.googleapis.com/udpa.type.v1.TypedStruct"; 67 static final String TYPE_URL_TYPED_STRUCT = 68 "type.googleapis.com/xds.type.v3.TypedStruct"; 69 70 @Nullable extractResourceName(Message unpackedResource)71 abstract String extractResourceName(Message unpackedResource); 72 unpackedClassName()73 abstract Class<? extends com.google.protobuf.Message> unpackedClassName(); 74 typeName()75 abstract String typeName(); 76 typeUrl()77 abstract String typeUrl(); 78 79 // Do not confuse with the SotW approach: it is the mechanism in which the client must specify all 80 // resource names it is interested in with each request. Different resource types may behave 81 // differently in this approach. For LDS and CDS resources, the server must return all resources 82 // that the client has subscribed to in each request. For RDS and EDS, the server may only return 83 // the resources that need an update. isFullStateOfTheWorld()84 abstract boolean isFullStateOfTheWorld(); 85 86 static class Args { 87 final ServerInfo serverInfo; 88 final String versionInfo; 89 final String nonce; 90 final Bootstrapper.BootstrapInfo bootstrapInfo; 91 final FilterRegistry filterRegistry; 92 final LoadBalancerRegistry loadBalancerRegistry; 93 final TlsContextManager tlsContextManager; 94 // Management server is required to always send newly requested resources, even if they 95 // may have been sent previously (proactively). Thus, client does not need to cache 96 // unrequested resources. 97 // Only resources in the set needs to be parsed. Null means parse everything. 98 final @Nullable Set<String> subscribedResources; 99 Args(ServerInfo serverInfo, String versionInfo, String nonce, Bootstrapper.BootstrapInfo bootstrapInfo, FilterRegistry filterRegistry, LoadBalancerRegistry loadBalancerRegistry, TlsContextManager tlsContextManager, @Nullable Set<String> subscribedResources)100 public Args(ServerInfo serverInfo, String versionInfo, String nonce, 101 Bootstrapper.BootstrapInfo bootstrapInfo, 102 FilterRegistry filterRegistry, 103 LoadBalancerRegistry loadBalancerRegistry, 104 TlsContextManager tlsContextManager, 105 @Nullable Set<String> subscribedResources) { 106 this.serverInfo = serverInfo; 107 this.versionInfo = versionInfo; 108 this.nonce = nonce; 109 this.bootstrapInfo = bootstrapInfo; 110 this.filterRegistry = filterRegistry; 111 this.loadBalancerRegistry = loadBalancerRegistry; 112 this.tlsContextManager = tlsContextManager; 113 this.subscribedResources = subscribedResources; 114 } 115 } 116 parse(Args args, List<Any> resources)117 ValidatedResourceUpdate<T> parse(Args args, List<Any> resources) { 118 Map<String, ParsedResource<T>> parsedResources = new HashMap<>(resources.size()); 119 Set<String> unpackedResources = new HashSet<>(resources.size()); 120 Set<String> invalidResources = new HashSet<>(); 121 List<String> errors = new ArrayList<>(); 122 123 for (int i = 0; i < resources.size(); i++) { 124 Any resource = resources.get(i); 125 126 Message unpackedMessage; 127 try { 128 resource = maybeUnwrapResources(resource); 129 unpackedMessage = unpackCompatibleType(resource, unpackedClassName(), typeUrl(), null); 130 } catch (InvalidProtocolBufferException e) { 131 errors.add(String.format("%s response Resource index %d - can't decode %s: %s", 132 typeName(), i, unpackedClassName().getSimpleName(), e.getMessage())); 133 continue; 134 } 135 String name = extractResourceName(unpackedMessage); 136 if (name == null || !isResourceNameValid(name, resource.getTypeUrl())) { 137 errors.add( 138 "Unsupported resource name: " + name + " for type: " + typeName()); 139 continue; 140 } 141 String cname = canonifyResourceName(name); 142 if (args.subscribedResources != null && !args.subscribedResources.contains(name)) { 143 continue; 144 } 145 unpackedResources.add(cname); 146 147 T resourceUpdate; 148 try { 149 resourceUpdate = doParse(args, unpackedMessage); 150 } catch (XdsClientImpl.ResourceInvalidException e) { 151 errors.add(String.format("%s response %s '%s' validation error: %s", 152 typeName(), unpackedClassName().getSimpleName(), cname, e.getMessage())); 153 invalidResources.add(cname); 154 continue; 155 } 156 157 // Resource parsed successfully. 158 parsedResources.put(cname, new ParsedResource<T>(resourceUpdate, resource)); 159 } 160 return new ValidatedResourceUpdate<T>(parsedResources, unpackedResources, invalidResources, 161 errors); 162 163 } 164 doParse(Args args, Message unpackedMessage)165 abstract T doParse(Args args, Message unpackedMessage) throws ResourceInvalidException; 166 167 /** 168 * Helper method to unpack serialized {@link com.google.protobuf.Any} message, while replacing 169 * Type URL {@code compatibleTypeUrl} with {@code typeUrl}. 170 * 171 * @param <T> The type of unpacked message 172 * @param any serialized message to unpack 173 * @param clazz the class to unpack the message to 174 * @param typeUrl type URL to replace message Type URL, when it's compatible 175 * @param compatibleTypeUrl compatible Type URL to be replaced with {@code typeUrl} 176 * @return Unpacked message 177 * @throws InvalidProtocolBufferException if the message couldn't be unpacked 178 */ unpackCompatibleType( Any any, Class<T> clazz, String typeUrl, String compatibleTypeUrl)179 static <T extends com.google.protobuf.Message> T unpackCompatibleType( 180 Any any, Class<T> clazz, String typeUrl, String compatibleTypeUrl) 181 throws InvalidProtocolBufferException { 182 if (any.getTypeUrl().equals(compatibleTypeUrl)) { 183 any = any.toBuilder().setTypeUrl(typeUrl).build(); 184 } 185 return any.unpack(clazz); 186 } 187 maybeUnwrapResources(Any resource)188 private Any maybeUnwrapResources(Any resource) 189 throws InvalidProtocolBufferException { 190 if (resource.getTypeUrl().equals(TYPE_URL_RESOURCE)) { 191 return unpackCompatibleType(resource, Resource.class, TYPE_URL_RESOURCE, 192 null).getResource(); 193 } else { 194 return resource; 195 } 196 } 197 198 static final class ParsedResource<T extends ResourceUpdate> { 199 private final T resourceUpdate; 200 private final Any rawResource; 201 ParsedResource(T resourceUpdate, Any rawResource)202 public ParsedResource(T resourceUpdate, Any rawResource) { 203 this.resourceUpdate = checkNotNull(resourceUpdate, "resourceUpdate"); 204 this.rawResource = checkNotNull(rawResource, "rawResource"); 205 } 206 getResourceUpdate()207 T getResourceUpdate() { 208 return resourceUpdate; 209 } 210 getRawResource()211 Any getRawResource() { 212 return rawResource; 213 } 214 } 215 216 static final class ValidatedResourceUpdate<T extends ResourceUpdate> { 217 Map<String, ParsedResource<T>> parsedResources; 218 Set<String> unpackedResources; 219 Set<String> invalidResources; 220 List<String> errors; 221 222 // validated resource update ValidatedResourceUpdate(Map<String, ParsedResource<T>> parsedResources, Set<String> unpackedResources, Set<String> invalidResources, List<String> errors)223 public ValidatedResourceUpdate(Map<String, ParsedResource<T>> parsedResources, 224 Set<String> unpackedResources, 225 Set<String> invalidResources, 226 List<String> errors) { 227 this.parsedResources = parsedResources; 228 this.unpackedResources = unpackedResources; 229 this.invalidResources = invalidResources; 230 this.errors = errors; 231 } 232 } 233 getFlag(String envVarName, boolean enableByDefault)234 private static boolean getFlag(String envVarName, boolean enableByDefault) { 235 String envVar = System.getenv(envVarName); 236 if (enableByDefault) { 237 return Strings.isNullOrEmpty(envVar) || Boolean.parseBoolean(envVar); 238 } else { 239 return !Strings.isNullOrEmpty(envVar) && Boolean.parseBoolean(envVar); 240 } 241 } 242 243 @VisibleForTesting 244 static final class StructOrError<T> { 245 246 /** 247 * Returns a {@link StructOrError} for the successfully converted data object. 248 */ fromStruct(T struct)249 static <T> StructOrError<T> fromStruct(T struct) { 250 return new StructOrError<>(struct); 251 } 252 253 /** 254 * Returns a {@link StructOrError} for the failure to convert the data object. 255 */ fromError(String errorDetail)256 static <T> StructOrError<T> fromError(String errorDetail) { 257 return new StructOrError<>(errorDetail); 258 } 259 260 private final String errorDetail; 261 private final T struct; 262 StructOrError(T struct)263 private StructOrError(T struct) { 264 this.struct = checkNotNull(struct, "struct"); 265 this.errorDetail = null; 266 } 267 StructOrError(String errorDetail)268 private StructOrError(String errorDetail) { 269 this.struct = null; 270 this.errorDetail = checkNotNull(errorDetail, "errorDetail"); 271 } 272 273 /** 274 * Returns struct if exists, otherwise null. 275 */ 276 @VisibleForTesting 277 @Nullable getStruct()278 T getStruct() { 279 return struct; 280 } 281 282 /** 283 * Returns error detail if exists, otherwise null. 284 */ 285 @VisibleForTesting 286 @Nullable getErrorDetail()287 String getErrorDetail() { 288 return errorDetail; 289 } 290 } 291 } 292