| // 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))); |
| } |
| } |