diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2021-09-22 19:24:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-22 19:24:50 +0200 |
commit | f024e11bd602335db7a22492e4d54841f7fea82f (patch) | |
tree | 6ca583381e510bc8b6bc63a8ac227fb7044b072d /flags | |
parent | a781b8c32eb6b470bdc64c281de9ba29987a1711 (diff) | |
parent | 01a79eb963ea6c65efa3ba21807d3ced2f79defc (diff) |
Merge pull request #19249 from vespa-engine/freva/user-flagv7.472.1
Expose user feature flags in /user/v1/user response
Diffstat (limited to 'flags')
8 files changed, 100 insertions, 31 deletions
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java index ede7bd6a109..5b3b2a94beb 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java @@ -4,9 +4,7 @@ package com.yahoo.vespa.flags; import com.yahoo.vespa.flags.json.DimensionHelper; import javax.annotation.concurrent.Immutable; -import java.util.Collections; import java.util.EnumMap; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -72,15 +70,15 @@ public class FetchVector { private final Map<Dimension, String> map; public FetchVector() { - this.map = Collections.emptyMap(); + this.map = Map.of(); } public static FetchVector fromMap(Map<Dimension, String> map) { - return new FetchVector(new HashMap<>(map)); + return new FetchVector(map); } private FetchVector(Map<Dimension, String> map) { - this.map = Collections.unmodifiableMap(map); + this.map = Map.copyOf(map); } public Optional<String> getValue(Dimension dimension) { @@ -93,6 +91,10 @@ public class FetchVector { public boolean isEmpty() { return map.isEmpty(); } + public boolean hasDimension(FetchVector.Dimension dimension) { + return map.containsKey(dimension); + } + /** * Returns a new FetchVector, identical to {@code this} except for its value in {@code dimension}. * Dimension is removed if the value is null. diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java index d01ca64cb9f..7ddbd85a904 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java @@ -3,9 +3,9 @@ package com.yahoo.vespa.flags; import javax.annotation.concurrent.Immutable; import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; +import java.util.EnumSet; import java.util.List; +import java.util.Set; /** * @author hakonhall @@ -28,14 +28,14 @@ public class FlagDefinition { String description, String modificationEffect, FetchVector.Dimension... dimensions) { - validate(owners, createdAt, expiresAt); this.unboundFlag = unboundFlag; this.owners = owners; this.createdAt = createdAt; this.expiresAt = expiresAt; this.description = description; this.modificationEffect = modificationEffect; - this.dimensions = Collections.unmodifiableList(Arrays.asList(dimensions)); + this.dimensions = List.of(dimensions); + validate(owners, createdAt, expiresAt, this.dimensions); } public UnboundFlag<?, ?, ?> getUnboundFlag() { @@ -60,13 +60,14 @@ public class FlagDefinition { public Instant getExpiresAt() { return expiresAt; } - private static void validate(List<String> owners, Instant createdAt, Instant expiresAt) { + private static void validate(List<String> owners, Instant createdAt, Instant expiresAt, List<FetchVector.Dimension> dimensions) { if (expiresAt.isBefore(createdAt)) { throw new IllegalArgumentException( String.format( "Flag cannot expire before its creation date (createdAt='%s', expiresAt='%s')", createdAt, expiresAt)); } + if (owners == PermanentFlags.OWNERS) { if (!createdAt.equals(PermanentFlags.CREATED_AT) || !expiresAt.equals(PermanentFlags.EXPIRES_AT)) { throw new IllegalArgumentException("Invalid creation or expiration date for permanent flag"); @@ -74,5 +75,15 @@ public class FlagDefinition { } else if (owners.isEmpty()) { throw new IllegalArgumentException("Owner(s) must be specified"); } + + if (dimensions.contains(FetchVector.Dimension.CONSOLE_USER_EMAIL)) { + Set<FetchVector.Dimension> disallowedCombinations = EnumSet.allOf(FetchVector.Dimension.class); + disallowedCombinations.remove(FetchVector.Dimension.CONSOLE_USER_EMAIL); + disallowedCombinations.remove(FetchVector.Dimension.APPLICATION_ID); + disallowedCombinations.remove(FetchVector.Dimension.TENANT_ID); + disallowedCombinations.retainAll(dimensions); + if (!disallowedCombinations.isEmpty()) + throw new IllegalArgumentException("Dimension " + FetchVector.Dimension.CONSOLE_USER_EMAIL + " cannot be combined with " + disallowedCombinations); + } } } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 30110493da2..d4157b659ae 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.TreeMap; import static com.yahoo.vespa.flags.FetchVector.Dimension.APPLICATION_ID; +import static com.yahoo.vespa.flags.FetchVector.Dimension.CONSOLE_USER_EMAIL; import static com.yahoo.vespa.flags.FetchVector.Dimension.HOSTNAME; import static com.yahoo.vespa.flags.FetchVector.Dimension.TENANT_ID; import static com.yahoo.vespa.flags.FetchVector.Dimension.VESPA_VERSION; @@ -278,7 +279,7 @@ public class Flags { List.of("olaa"), "2021-09-13", "2021-12-31", "Enable Horizon dashboard", "Takes effect immediately", - TENANT_ID + TENANT_ID, CONSOLE_USER_EMAIL ); public static final UnboundBooleanFlag ENABLE_ONPREM_TENANT_S3_ARCHIVE = defineFeatureFlag( @@ -416,8 +417,8 @@ public class Flags { * * <p>NOT thread-safe. Tests using this cannot run in parallel. */ - public static Replacer clearFlagsForTesting() { - return new Replacer(); + public static Replacer clearFlagsForTesting(FlagId... flagsToKeep) { + return new Replacer(flagsToKeep); } public static class Replacer implements AutoCloseable { @@ -425,10 +426,11 @@ public class Flags { private final TreeMap<FlagId, FlagDefinition> savedFlags; - private Replacer() { + private Replacer(FlagId... flagsToKeep) { verifyAndSetFlagsCleared(true); this.savedFlags = Flags.flags; Flags.flags = new TreeMap<>(); + List.of(flagsToKeep).forEach(id -> Flags.flags.put(id, savedFlags.get(id))); } @Override diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java index 46961fbd8cc..f73e0033773 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java @@ -52,6 +52,16 @@ public interface Condition extends Predicate<FetchVector> { public FetchVector.Dimension dimension() { return dimension; } public List<String> values() { return values; } public Optional<String> predicate() { return predicate; } + + public Condition createAs(Condition.Type type) { + switch (type) { + case WHITELIST: return WhitelistCondition.create(this); + case BLACKLIST: return BlacklistCondition.create(this); + case RELATIONAL: return RelationalCondition.create(this); + } + + throw new IllegalArgumentException("Unknown type '" + type + "'"); + } } static Condition fromWire(WireCondition wireCondition) { @@ -70,14 +80,14 @@ public interface Condition extends Predicate<FetchVector> { params.withPredicate(wireCondition.predicate); } - switch (type) { - case WHITELIST: return WhitelistCondition.create(params); - case BLACKLIST: return BlacklistCondition.create(params); - case RELATIONAL: return RelationalCondition.create(params); - } - - throw new IllegalArgumentException("Unknown type '" + type + "'"); + return params.createAs(type); } + Condition.Type type(); + + FetchVector.Dimension dimension(); + + CreateParams toCreateParams(); + WireCondition toWire(); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java index c4079380a8c..eea61eb71ef 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java @@ -14,9 +14,6 @@ import com.yahoo.vespa.flags.json.wire.WireRule; import javax.annotation.concurrent.Immutable; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -35,16 +32,16 @@ public class FlagData { private final FetchVector defaultFetchVector; public FlagData(FlagId id) { - this(id, new FetchVector(), Collections.emptyList()); + this(id, new FetchVector(), List.of()); } public FlagData(FlagId id, FetchVector defaultFetchVector, Rule... rules) { - this(id, defaultFetchVector, Arrays.asList(rules)); + this(id, defaultFetchVector, List.of(rules)); } public FlagData(FlagId id, FetchVector defaultFetchVector, List<Rule> rules) { this.id = id; - this.rules = Collections.unmodifiableList(new ArrayList<>(rules)); + this.rules = List.copyOf(rules); this.defaultFetchVector = defaultFetchVector; } @@ -52,6 +49,10 @@ public class FlagData { return id; } + public List<Rule> rules() { + return rules; + } + public boolean isEmpty() { return rules.isEmpty() && defaultFetchVector.isEmpty(); } public Optional<RawFlag> resolve(FetchVector fetchVector) { @@ -136,7 +137,7 @@ public class FlagData { } private static List<Rule> rulesFromWire(List<WireRule> wireRules) { - if (wireRules == null) return Collections.emptyList(); + if (wireRules == null) return List.of(); return wireRules.stream().map(Rule::fromWire).collect(Collectors.toList()); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java index c2c76529833..136857bea5f 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java @@ -27,6 +27,21 @@ public abstract class ListCondition implements Condition { } @Override + public Type type() { + return type; + } + + @Override + public FetchVector.Dimension dimension() { + return dimension; + } + + @Override + public CreateParams toCreateParams() { + return new CreateParams(dimension).withValues(values); + } + + @Override public boolean test(FetchVector fetchVector) { boolean listContainsValue = fetchVector.getValue(dimension).map(values::contains).orElse(false); return isWhitelist == listContainsValue; diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java index db2f0a3a197..4ed3e49029f 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java @@ -48,6 +48,21 @@ public class RelationalCondition implements Condition { } @Override + public Type type() { + return Type.RELATIONAL; + } + + @Override + public FetchVector.Dimension dimension() { + return dimension; + } + + @Override + public CreateParams toCreateParams() { + return new CreateParams(dimension).withPredicate(relationalPredicate.toWire()); + } + + @Override public boolean test(FetchVector fetchVector) { return fetchVector.getValue(dimension).map(predicate::test).orElse(false); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java index b7d60889419..0d50f1e283f 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java @@ -6,7 +6,6 @@ import com.yahoo.vespa.flags.JsonNodeRawFlag; import com.yahoo.vespa.flags.RawFlag; import com.yahoo.vespa.flags.json.wire.WireRule; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -20,18 +19,32 @@ public class Rule { private final Optional<RawFlag> valueToApply; public Rule(Optional<RawFlag> valueToApply, Condition... andConditions) { - this(valueToApply, Arrays.asList(andConditions)); + this(valueToApply, List.of(andConditions)); } public Rule(Optional<RawFlag> valueToApply, List<Condition> andConditions) { - this.andConditions = andConditions; + this.andConditions = List.copyOf(andConditions); this.valueToApply = valueToApply; } + public List<Condition> conditions() { + return andConditions; + } + + /** Returns true if all the conditions satisfy the given fetch vector */ public boolean match(FetchVector fetchVector) { return andConditions.stream().allMatch(condition -> condition.test(fetchVector)); } + /** + * Returns true if all the conditions on dimensions set in the fetch vector are satisfied. + * Conditions on dimensions not specified in the given fetch vector are ignored. + */ + public boolean partialMatch(FetchVector fetchVector) { + return andConditions.stream() + .allMatch(condition -> !fetchVector.hasDimension(condition.dimension()) || condition.test(fetchVector)); + } + public Optional<RawFlag> getValueToApply() { return valueToApply; } |