| // Copyright 2026 The Bazel Authors. All rights reserved. | |
| // | |
| // 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 com.google.devtools.build.lib.remote; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import build.bazel.remote.asset.v1.FetchGrpc; | |
| import build.bazel.remote.execution.v2.ActionCacheGrpc; | |
| import build.bazel.remote.execution.v2.CapabilitiesGrpc; | |
| import build.bazel.remote.execution.v2.ContentAddressableStorageGrpc; | |
| import com.google.bytestream.ByteStreamGrpc; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.devtools.build.lib.remote.options.RemoteOptions; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.gson.Gson; | |
| import com.google.gson.JsonArray; | |
| import com.google.gson.JsonElement; | |
| import com.google.gson.JsonObject; | |
| import com.google.gson.JsonParseException; | |
| import com.google.gson.JsonParser; | |
| import com.google.gson.reflect.TypeToken; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.io.Reader; | |
| import java.lang.reflect.Type; | |
| import java.time.Duration; | |
| import java.util.Map; | |
| /** Builds Bazel's generated gRPC service config for remote services. */ | |
| public final class RemoteGrpcServiceConfig { | |
| private static final Gson GSON = new Gson(); | |
| private static final Type MAP_TYPE = new TypeToken<Map<String, ?>>() {}.getType(); | |
| private static final ImmutableSet<String> SUPPORTED_TOP_LEVEL_FIELDS = | |
| ImmutableSet.of("methodConfig"); | |
| private static final ImmutableSet<String> SUPPORTED_METHOD_CONFIG_FIELDS = | |
| ImmutableSet.of("name", "timeout"); | |
| private static final ImmutableSet<String> SUPPORTED_NAME_FIELDS = | |
| ImmutableSet.of("service", "method"); | |
| private RemoteGrpcServiceConfig() {} | |
| public static ImmutableMap<String, ?> create(RemoteOptions options) { | |
| return create(options.getRemoteTimeout()); | |
| } | |
| public static ImmutableMap<String, ?> create(RemoteOptions options, Path workingDirectory) | |
| throws IOException { | |
| PathFragment serviceConfigPath = options.getRemoteGrpcServiceConfig(); | |
| if (serviceConfigPath == null) { | |
| return create(options); | |
| } | |
| return parse(workingDirectory.getRelative(serviceConfigPath)); | |
| } | |
| static ImmutableMap<String, ?> create(Duration remoteTimeout) { | |
| return ImmutableMap.of( | |
| "methodConfig", | |
| ImmutableList.of( | |
| ImmutableMap.of( | |
| "name", | |
| ImmutableList.of( | |
| ImmutableMap.of("service", ActionCacheGrpc.SERVICE_NAME), | |
| ImmutableMap.of("service", CapabilitiesGrpc.SERVICE_NAME), | |
| ImmutableMap.of("service", ContentAddressableStorageGrpc.SERVICE_NAME), | |
| ImmutableMap.of("service", ByteStreamGrpc.SERVICE_NAME), | |
| ImmutableMap.of("service", FetchGrpc.SERVICE_NAME)), | |
| "timeout", | |
| remoteTimeout.toSeconds() + "s"))); | |
| } | |
| private static ImmutableMap<String, ?> parse(Path serviceConfigPath) throws IOException { | |
| try (Reader reader = new InputStreamReader(serviceConfigPath.getInputStream(), UTF_8)) { | |
| JsonObject root = requireObject(JsonParser.parseReader(reader), "service config"); | |
| rejectUnsupportedFields(root, SUPPORTED_TOP_LEVEL_FIELDS, "service config"); | |
| rejectUnsupportedMethodConfigFields(root); | |
| Map<String, ?> serviceConfig = GSON.fromJson(root, MAP_TYPE); | |
| return ImmutableMap.copyOf(serviceConfig); | |
| } catch (JsonParseException e) { | |
| throw new IOException( | |
| "failed to parse " + serviceConfigPath.getPathString() + ": " + e.getMessage(), e); | |
| } | |
| } | |
| private static void rejectUnsupportedMethodConfigFields(JsonObject rootObject) | |
| throws IOException { | |
| JsonElement methodConfigsElement = rootObject.get("methodConfig"); | |
| if (methodConfigsElement == null || !methodConfigsElement.isJsonArray()) { | |
| return; | |
| } | |
| JsonArray methodConfigs = methodConfigsElement.getAsJsonArray(); | |
| for (int i = 0; i < methodConfigs.size(); ++i) { | |
| JsonElement methodConfigElement = methodConfigs.get(i); | |
| if (!methodConfigElement.isJsonObject()) { | |
| continue; | |
| } | |
| JsonObject methodConfig = methodConfigElement.getAsJsonObject(); | |
| String methodConfigPath = "methodConfig[" + i + "]"; | |
| rejectUnsupportedFields(methodConfig, SUPPORTED_METHOD_CONFIG_FIELDS, methodConfigPath); | |
| rejectUnsupportedNameFields(methodConfig, methodConfigPath); | |
| } | |
| } | |
| private static void rejectUnsupportedNameFields(JsonObject methodConfig, String methodConfigPath) | |
| throws IOException { | |
| JsonElement namesElement = methodConfig.get("name"); | |
| if (namesElement == null || !namesElement.isJsonArray()) { | |
| return; | |
| } | |
| JsonArray names = namesElement.getAsJsonArray(); | |
| for (int i = 0; i < names.size(); ++i) { | |
| JsonElement nameElement = names.get(i); | |
| if (nameElement.isJsonObject()) { | |
| rejectUnsupportedFields( | |
| nameElement.getAsJsonObject(), | |
| SUPPORTED_NAME_FIELDS, | |
| methodConfigPath + ".name[" + i + "]"); | |
| } | |
| } | |
| } | |
| private static void rejectUnsupportedFields( | |
| JsonObject object, ImmutableSet<String> supportedFields, String path) throws IOException { | |
| for (Map.Entry<String, JsonElement> entry : object.entrySet()) { | |
| if (!supportedFields.contains(entry.getKey())) { | |
| throw new IOException(path + " contains unsupported field '" + entry.getKey() + "'"); | |
| } | |
| } | |
| } | |
| private static JsonObject requireObject(JsonElement element, String path) throws IOException { | |
| if (element == null || !element.isJsonObject()) { | |
| throw new IOException(path + " must be an object"); | |
| } | |
| return element.getAsJsonObject(); | |
| } | |
| } |