blob: 38d48cc185027ffc0f5bd063230de0911c9eea34 [file] [log] [blame]
// Copyright 2015 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.runtime;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Strings;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.AllowValues;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.DisallowValues;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue;
import com.google.devtools.common.options.OptionPriority;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParser.OptionDescription;
import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsProvider;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.TextFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Enforces the {@link FlagPolicy}s (from an {@link InvocationPolicy} proto) on an
* {@link OptionsParser} by validating and changing the flag values in the given
* {@link OptionsParser}.
*
* <p>"Flag" and "Option" are used interchangeably in this file.
*/
public final class InvocationPolicyEnforcer {
/**
* Creates an {@link InvocationPolicyEnforcer} with the invocation policy obtained from the given
* {@link OptionsProvider}. This uses the provider only to obtain the policy from the
* --invocation_policy flag and does not enforce any policy on the flags in the provider.
*
* @param startupOptionsProvider an options provider which provides a BlazeServerStartupOptions
* options class.
* @throws OptionsParsingException if the value of --invocation_policy is invalid.
*/
public static InvocationPolicyEnforcer create(OptionsProvider startupOptionsProvider)
throws OptionsParsingException {
BlazeServerStartupOptions blazeServerStartupOptions =
startupOptionsProvider.getOptions(BlazeServerStartupOptions.class);
return new InvocationPolicyEnforcer(parsePolicy(blazeServerStartupOptions.invocationPolicy));
}
public static InvocationPolicyEnforcer create(String invocationPolicy)
throws OptionsParsingException {
return new InvocationPolicyEnforcer(parsePolicy(invocationPolicy));
}
/**
* Parses the given InvocationPolicy string, which may be a base64-encoded binary-serialized
* InvocationPolicy message, or a text formatted InvocationPolicy message. Note that the
* text format is not backwards compatible as the binary format is.
*
* @throws OptionsParsingException if the value of --invocation_policy is invalid.
*/
private static InvocationPolicy parsePolicy(String policy) throws OptionsParsingException {
if (Strings.isNullOrEmpty(policy)) {
return null;
}
try {
try {
// First try decoding the policy as a base64 encoded binary proto.
return InvocationPolicy.parseFrom(
BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(policy)));
} catch (IllegalArgumentException e) {
// If the flag value can't be decoded from base64, try decoding the policy as a text
// formatted proto.
InvocationPolicy.Builder builder = InvocationPolicy.newBuilder();
TextFormat.merge(policy, builder);
return builder.build();
}
} catch (InvalidProtocolBufferException | TextFormat.ParseException e) {
throw new OptionsParsingException("Malformed value of --invocation_policy: " + policy, e);
}
}
private static final Logger log = Logger.getLogger(InvocationPolicyEnforcer.class.getName());
private static final Function<Object, String> INVOCATION_POLICY_SOURCE =
Functions.constant("Invocation policy");
@Nullable
private final InvocationPolicy invocationPolicy;
/**
* Creates an InvocationPolicyEnforcer that enforces the given policy.
*
* @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
* nothing in calls to enforce().
*/
public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy) {
this.invocationPolicy = invocationPolicy;
}
/**
* Applies this OptionsPolicyEnforcer's policy to the given OptionsParser for all blaze commands.
*
* @param parser The OptionsParser to enforce policy on.
* @throws OptionsParsingException if any flag policy is invalid.
*/
public void enforce(OptionsParser parser) throws OptionsParsingException {
enforce(parser, "");
}
/**
* Applies this OptionsPolicyEnforcer's policy to the given OptionsParser.
*
* @param parser The OptionsParser to enforce policy on.
* @param command The current blaze command, for flag policies that apply to only specific
* commands.
* @throws OptionsParsingException if any flag policy is invalid.
*/
public void enforce(OptionsParser parser, String command) throws OptionsParsingException {
if (invocationPolicy == null) {
return;
}
if (invocationPolicy.getFlagPoliciesCount() == 0) {
log.warning("InvocationPolicy contains no flag policies.");
return;
}
for (FlagPolicy flagPolicy : invocationPolicy.getFlagPoliciesList()) {
String flagName = flagPolicy.getFlagName();
// Skip the flag policy if it doesn't apply to this command. If the commands list is empty,
// then the policy applies to all commands.
if (!flagPolicy.getCommandsList().isEmpty()
&& !flagPolicy.getCommandsList().contains(command)) {
log.info(String.format("Skipping flag policy for flag '%s' because it "
+ "applies only to commands %s and the current command is '%s'",
flagName, flagPolicy.getCommandsList(), command));
continue;
}
OptionValueDescription valueDescription;
try {
valueDescription = parser.getOptionValueDescription(flagName);
} catch (IllegalArgumentException e) {
// This flag doesn't exist. We are deliberately lenient if the flag policy has a flag
// we don't know about. This is for better future proofing so that as new flags are added,
// new policies can use the new flags without worrying about older versions of Bazel.
log.info(String.format(
"Flag '%s' specified by invocation policy does not exist", flagName));
continue;
}
OptionDescription optionDescription = parser.getOptionDescription(flagName);
// getOptionDescription() will return null if the option does not exist, however
// getOptionValueDescription() above would have thrown an IllegalArgumentException if that
// were the case.
Verify.verifyNotNull(optionDescription);
switch (flagPolicy.getOperationCase()) {
case SET_VALUE:
applySetValueOperation(parser, flagPolicy, flagName,
valueDescription, optionDescription);
break;
case USE_DEFAULT:
applyUseDefaultOperation(parser, flagName);
break;
case ALLOW_VALUES:
AllowValues allowValues = flagPolicy.getAllowValues();
FilterValueOperation.ALLOW_VALUE_OPERATION.apply(
parser,
allowValues.getAllowedValuesList(),
allowValues.hasNewDefaultValue() ? allowValues.getNewDefaultValue() : null,
flagName,
valueDescription,
optionDescription);
break;
case DISALLOW_VALUES:
DisallowValues disallowValues = flagPolicy.getDisallowValues();
FilterValueOperation.DISALLOW_VALUE_OPERATION.apply(
parser,
disallowValues.getDisallowedValuesList(),
disallowValues.hasNewDefaultValue() ? disallowValues.getNewDefaultValue() : null,
flagName,
valueDescription,
optionDescription);
break;
case OPERATION_NOT_SET:
throw new OptionsParsingException(String.format("Flag policy for flag '%s' does not "
+ "have an operation", flagName));
default:
log.warning(String.format("Unknown operation '%s' from invocation policy for flag '%s'",
flagPolicy.getOperationCase(), flagName));
break;
}
}
}
private static void applySetValueOperation(
OptionsParser parser,
FlagPolicy flagPolicy,
String flagName,
OptionValueDescription valueDescription,
OptionDescription optionDescription) throws OptionsParsingException {
SetValue setValue = flagPolicy.getSetValue();
// SetValue.flag_value must have at least 1 value.
if (setValue.getFlagValueCount() == 0) {
throw new OptionsParsingException(String.format(
"SetValue operation from invocation policy for flag '%s' does not have a value",
flagName));
}
// Flag must allow multiple values if multiple values are specified by the policy.
if (setValue.getFlagValueCount() > 1 && !optionDescription.getAllowMultiple()) {
throw new OptionsParsingException(String.format(
"SetValue operation from invocation policy sets multiple values for flag '%s' which "
+ "does not allow multiple values", flagName));
}
if (setValue.getOverridable() && valueDescription != null) {
// The user set the value for the flag but the flag policy is overridable, so keep the user's
// value.
log.info(String.format("Keeping value '%s' from source '%s' for flag '%s' "
+ "because the invocation policy specifying the value(s) '%s' is overridable",
valueDescription.getValue(), valueDescription.getSource(), flagName,
setValue.getFlagValueList()));
} else {
// Clear the value in case the flag is a repeated flag (so that values don't accumulate), and
// in case the flag is an expansion flag or has implicit flags (so that the additional flags
// also get cleared).
parser.clearValue(flagName);
// Set all the flag values from the policy.
for (String flagValue : setValue.getFlagValueList()) {
if (valueDescription == null) {
log.info(String.format("Setting value for flag '%s' from invocation "
+ "policy to '%s', overriding the default value '%s'", flagName, flagValue,
optionDescription.getDefaultValue()));
} else {
log.info(String.format("Setting value for flag '%s' from invocation "
+ "policy to '%s', overriding value '%s' from '%s'", flagName, flagValue,
valueDescription.getValue(), valueDescription.getSource()));
}
setFlagValue(parser, flagName, flagValue);
}
}
}
private static void applyUseDefaultOperation(OptionsParser parser, String flagName) {
Map<String, OptionValueDescription> clearedValues = parser.clearValue(flagName);
for (Entry<String, OptionValueDescription> clearedValue : clearedValues.entrySet()) {
OptionValueDescription clearedValueDescription = clearedValue.getValue();
String clearedFlagName = clearedValue.getKey();
String originalValue = clearedValueDescription.getValue().toString();
String source = clearedValueDescription.getSource();
Object clearedFlagDefaultValue = parser.getOptionDescription(clearedFlagName)
.getDefaultValue();
log.info(String.format("Using default value '%s' for flag '%s' as "
+ "specified by invocation policy, overriding original value '%s' from '%s'",
clearedFlagDefaultValue, clearedFlagName, originalValue, source));
}
}
/**
* Checks the user's flag values against a filtering function.
*/
private abstract static class FilterValueOperation {
private static final FilterValueOperation ALLOW_VALUE_OPERATION =
new FilterValueOperation("Allow") {
@Override
boolean filter(Set<Object> convertedPolicyValues, Object value) {
return convertedPolicyValues.contains(value);
}
};
private static final FilterValueOperation DISALLOW_VALUE_OPERATION =
new FilterValueOperation("Disallow") {
@Override
boolean filter(Set<Object> convertedPolicyValues, Object value) {
// In a disallow operation, the values that the flag policy specifies are not allowed, so
// the value is allowed if the set of policy values does not contain the current flag value.
return !convertedPolicyValues.contains(value);
}
};
private final String policyType;
FilterValueOperation(String policyType) {
this.policyType = policyType;
}
/**
* Determines if the given value is allowed.
*
* @param convertedPolicyValues The values given from the FlagPolicy, converted to real objects.
* @param value The user value of the flag.
* @return True if the value should be allowed, false if it should not.
*/
abstract boolean filter(Set<Object> convertedPolicyValues, Object value);
void apply(
OptionsParser parser,
List<String> policyValues,
String newDefaultValue,
String flagName,
OptionValueDescription valueDescription,
OptionDescription optionDescription) throws OptionsParsingException {
// Convert all the allowed values from strings to real objects using the options'
// converters so that they can be checked for equality using real .equals() instead
// of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-"
// (if the option has an abbreviation) are all equal for boolean flags. Plus converters
// can be arbitrarily complex.
Set<Object> convertedPolicyValues = Sets.newHashSet();
for (String value : policyValues) {
convertedPolicyValues.add(optionDescription.getConverter().convert(value));
}
if (valueDescription == null) {
// Nothing has set the value yet, so check that the default value from the flag's
// definition is allowed. The else case below (i.e. valueDescription is not null) checks for
// the flag allowing multiple values, however, flags that allow multiple values cannot have
// default values, and their value is always the empty list if they haven't been specified,
// which is why new_default_value is not a repeated field.
checkDefaultValue(
parser,
policyValues,
newDefaultValue,
flagName,
optionDescription,
convertedPolicyValues);
} else {
checkUserValue(
policyValues,
flagName,
valueDescription,
optionDescription,
convertedPolicyValues);
}
}
void checkDefaultValue(
OptionsParser parser,
List<String> policyValues,
String newDefaultValue,
String flagName,
OptionDescription optionDescription,
Set<Object> convertedPolicyValues) throws OptionsParsingException {
if (!filter(convertedPolicyValues, optionDescription.getDefaultValue())) {
if (newDefaultValue != null) {
// Use the default value from the policy.
log.info(String.format("Overriding default value '%s' for flag '%s' with "
+ "new default value '%s' specified by invocation policy. %sed values are: %s",
optionDescription.getDefaultValue(), flagName, newDefaultValue,
policyType, policyValues));
parser.clearValue(flagName);
setFlagValue(parser, flagName, newDefaultValue);
} else {
// The operation disallows the default value, but doesn't supply its own default.
throw new OptionsParsingException(String.format(
"Default flag value '%s' for flag '%s' is not allowed by invocation policy, but "
+ "the policy does not provide a new default value. "
+ "%sed values are: %s", optionDescription.getDefaultValue(), flagName,
policyType, policyValues));
}
}
}
void checkUserValue(
List<String> policyValues,
String flagName,
OptionValueDescription valueDescription,
OptionDescription optionDescription,
Set<Object> convertedPolicyValues) throws OptionsParsingException {
// Get the option values: there might be one of them or a list of them, so convert everything
// to a list (possibly of just the one value).
List<?> optionValues;
if (optionDescription.getAllowMultiple()) {
// allowMultiple requires that the type of the option be List<T>, so cast from Object
// to List<?>.
optionValues = (List<?>) valueDescription.getValue();
} else {
optionValues = ImmutableList.of(valueDescription.getValue());
}
for (Object value : optionValues) {
if (!filter(convertedPolicyValues, value)) {
throw new OptionsParsingException(String.format(
"Flag value '%s' for flag '%s' is not allowed by invocation policy. "
+ "%sed values are: %s", value, flagName, policyType, policyValues));
}
}
}
}
private static void setFlagValue(
OptionsParser parser,
String flagName,
String flagValue) throws OptionsParsingException {
parser.parseWithSourceFunction(OptionPriority.INVOCATION_POLICY, INVOCATION_POLICY_SOURCE,
Arrays.asList(String.format("--%s=%s", flagName, flagValue)));
}
}