blob: 0b88edaddf34f869c2de2412d39a8b955e2a92bd [file] [log] [blame]
// Copyright 2023 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 com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.remote.RemoteScrubbing.Config;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.protobuf.TextFormat;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.Nullable;
/**
* The {@link Scrubber} implements scrubbing of remote cache keys.
*
* <p>See the documentation for the {@code --experimental_remote_scrub_config} flag for more
* information.
*/
public class Scrubber {
/** An error that occurred while parsing the scrubbing configuration. */
public static class ConfigParseException extends Exception {
private ConfigParseException(String message, Throwable cause) {
super(message, cause);
}
}
private final ImmutableList<SpawnScrubber> spawnScrubbers;
@VisibleForTesting
Scrubber(Config configProto) {
ArrayList<SpawnScrubber> spawnScrubbers = new ArrayList<>();
for (Config.Rule ruleProto : configProto.getRulesList()) {
spawnScrubbers.add(new SpawnScrubber(ruleProto));
}
// Reverse the order so that later rules supersede earlier ones.
Collections.reverse(spawnScrubbers);
this.spawnScrubbers = ImmutableList.copyOf(spawnScrubbers);
}
/**
* Constructs a {@link Scrubber} from the given configuration file, which must contain a {@link
* Config} protocol buffer in text format.
*/
public static Scrubber parse(String configPath) throws ConfigParseException {
try (BufferedReader reader = Files.newBufferedReader(Paths.get(configPath))) {
var builder = Config.newBuilder();
TextFormat.getParser().merge(reader, builder);
return new Scrubber(builder.build());
} catch (IOException e) {
throw new ConfigParseException(e.getMessage(), e);
} catch (PatternSyntaxException e) {
throw new ConfigParseException(
String.format("in regex '%s': %s", e.getPattern(), e.getMessage()), e);
}
}
/**
* Returns a {@link SpawnScrubber} suitable for a {@link Spawn}, or {@code null} if the spawn does
* not need to be scrubbed.
*/
@Nullable
public SpawnScrubber forSpawn(Spawn spawn) {
for (SpawnScrubber spawnScrubber : spawnScrubbers) {
if (spawnScrubber.matches(spawn)) {
return spawnScrubber;
}
}
return null;
}
/**
* Encapsulates a set of transformations required to scrub the remote cache key for a set of
* spawns.
*/
public static class SpawnScrubber {
private final Pattern mnemonicPattern;
private final Pattern labelPattern;
private final Pattern kindPattern;
private final boolean matchTools;
private final ImmutableList<Pattern> omittedInputPatterns;
private final ImmutableMap<Pattern, String> argReplacements;
private final String salt;
private SpawnScrubber(Config.Rule ruleProto) {
Config.Matcher matcherProto = ruleProto.getMatcher();
this.mnemonicPattern = Pattern.compile(emptyToAll(matcherProto.getMnemonic()));
this.labelPattern = Pattern.compile(emptyToAll(matcherProto.getLabel()));
this.kindPattern = Pattern.compile(emptyToAll(matcherProto.getKind()));
this.matchTools = matcherProto.getMatchTools();
Config.Transform transformProto = ruleProto.getTransform();
this.omittedInputPatterns =
transformProto.getOmittedInputsList().stream()
.map(Pattern::compile)
.collect(toImmutableList());
this.argReplacements =
transformProto.getArgReplacementsList().stream()
.collect(toImmutableMap(r -> Pattern.compile(r.getSource()), r -> r.getTarget()));
this.salt = ruleProto.getTransform().getSalt();
}
private String emptyToAll(String s) {
return s.isEmpty() ? ".*" : s;
}
/** Whether this scrubber applies to the given {@link Spawn}. */
private boolean matches(Spawn spawn) {
String mnemonic = spawn.getMnemonic();
ActionOwner actionOwner = spawn.getResourceOwner().getOwner();
String label = actionOwner.getLabel().getCanonicalForm();
String kind = actionOwner.getTargetKind();
boolean isForTool = actionOwner.isBuildConfigurationForTool();
return (!isForTool || matchTools)
&& mnemonicPattern.matcher(mnemonic).matches()
&& labelPattern.matcher(label).matches()
&& kindPattern.matcher(kind).matches();
}
/** Whether an input with the given exec-relative path should be omitted from the cache key. */
public boolean shouldOmitInput(PathFragment execPath) {
for (Pattern pattern : omittedInputPatterns) {
if (pattern.matcher(execPath.getPathString()).matches()) {
return true;
}
}
return false;
}
/** Transforms a command line argument. */
public String transformArgument(String arg) {
for (Map.Entry<Pattern, String> entry : argReplacements.entrySet()) {
Pattern pattern = entry.getKey();
String replacement = entry.getValue();
// Don't use Pattern#replaceFirst because it allows references to capture groups.
Matcher m = pattern.matcher(arg);
if (m.find()) {
arg = arg.substring(0, m.start()) + replacement + arg.substring(m.end());
}
}
return arg;
}
/** Returns the scrubbing salt. */
public String getSalt() {
return salt;
}
}
}