summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@verizonmedia.com>2019-10-22 00:28:22 +0200
committerHåkon Hallingstad <hakon@verizonmedia.com>2019-10-22 00:28:22 +0200
commitc29b374c582c8b112a6e95150e5d4a460dc30464 (patch)
treea9ec0e5bb3a388d6c30199bd3b89b07b1bcb716e
parentfef3449937c173ef8128b6085829e0bbe3f97ce4 (diff)
Support flag conditions based on Vespa release
Supports a "relational" condition with a new dimension "vespa-version", that can be satisfied with e.g. "predicate": ">= 7.120.5" as long as the condition is evaluated in a JVM that has a Vtag at least high as 7.120.5. The typical use-case for this condition would be: The developer has used the flag to test and verify the feature is ready to roll out globally. The developer can now roll the feature with the next release, and ORCHESTRATED, halting if anything goes wrong like any normal rollout. This also allows one-shot tests of a feature flag in integration tests: Just enable it for an upcoming version with predicate "== 7.x.y".
-rw-r--r--configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java6
-rw-r--r--flags/pom.xml6
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java32
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java8
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/BlacklistCondition.java11
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java72
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java1
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java39
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java58
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/RelationalOperator.java39
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/RelationalPredicate.java43
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/WhitelistCondition.java11
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java1
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java40
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java12
15 files changed, 328 insertions, 51 deletions
diff --git a/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java
index 7460e42c866..e366c012a9e 100644
--- a/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java
+++ b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java
@@ -8,12 +8,15 @@ import com.yahoo.vespa.flags.JsonNodeRawFlag;
import com.yahoo.vespa.flags.json.Condition;
import com.yahoo.vespa.flags.json.FlagData;
import com.yahoo.vespa.flags.json.Rule;
+import com.yahoo.vespa.flags.json.WhitelistCondition;
import org.junit.Test;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
@@ -29,7 +32,8 @@ public class FlagsDbImplTest {
MockCurator curator = new MockCurator();
FlagsDbImpl db = new FlagsDbImpl(curator);
- Condition condition1 = new Condition(Condition.Type.WHITELIST, FetchVector.Dimension.HOSTNAME, "host1");
+ var params = new Condition.CreateParams(FetchVector.Dimension.HOSTNAME, List.of("host1"), Optional.empty());
+ Condition condition1 = new WhitelistCondition(params);
Rule rule1 = new Rule(Optional.of(JsonNodeRawFlag.fromJson("13")), condition1);
FlagId flagId = new FlagId("id");
FlagData data = new FlagData(flagId, new FetchVector().with(FetchVector.Dimension.ZONE_ID, "zone-a"), rule1);
diff --git a/flags/pom.xml b/flags/pom.xml
index c1e9eca20ab..6afa920e261 100644
--- a/flags/pom.xml
+++ b/flags/pom.xml
@@ -27,6 +27,12 @@
</dependency>
<dependency>
<groupId>com.yahoo.vespa</groupId>
+ <artifactId>component</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
<artifactId>defaults</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
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 d2228c58c51..a4fbec4922f 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java
@@ -24,16 +24,6 @@ public class FetchVector {
* Note: If this enum is changed, you must also change {@link DimensionHelper}.
*/
public enum Dimension {
- /**
- * WARNING: DO NOT USE
- *
- * <p>ALL flags can be set differently in different zones: This dimension is ONLY useful for the controller
- * that needs to handle multiple zones.
- *
- * <p>Value from ZoneId::value is of the form environment.region.
- */
- ZONE_ID,
-
/** Value from ApplicationId::serializedForm of the form tenant:applicationName:instance. */
APPLICATION_ID,
@@ -44,7 +34,27 @@ public class FetchVector {
NODE_TYPE,
/** Cluster type from com.yahoo.config.provision.ClusterSpec.Type::name, e.g. content, container, admin */
- CLUSTER_TYPE
+ CLUSTER_TYPE,
+
+ /**
+ * WARNING: DO SET THIS DIMENSION FOR A FLAG
+ *
+ * <p>ALL flags can be set differently in different zones: This dimension is ONLY useful for the controller
+ * that needs to handle multiple zones.
+ *
+ * <p>Value from ZoneId::value is of the form environment.region.
+ */
+ ZONE_ID,
+
+ /**
+ * WARNING: DO SET THIS DIMENSION FOR A FLAG
+ *
+ * <p>The Vespa version is always fetched implicitly from {@link com.yahoo.component.Vtag#currentVersion}.
+ *
+ * <p>Value from Version::toFullString is of the form Major.Minor.Micro[.qualifier]. When ordering
+ * versions, note that 7.3 == 7.3.0.
+ */
+ VESPA_VERSION
}
private final Map<Dimension, String> map;
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 a27171f29a2..ef5e8451650 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -1,6 +1,7 @@
// Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.flags;
+import com.yahoo.component.Vtag;
import com.yahoo.vespa.defaults.Defaults;
import com.yahoo.vespa.flags.custom.PreprovisionCapacity;
@@ -11,6 +12,7 @@ import java.util.TreeMap;
import static com.yahoo.vespa.flags.FetchVector.Dimension.APPLICATION_ID;
import static com.yahoo.vespa.flags.FetchVector.Dimension.HOSTNAME;
import static com.yahoo.vespa.flags.FetchVector.Dimension.NODE_TYPE;
+import static com.yahoo.vespa.flags.FetchVector.Dimension.VESPA_VERSION;
/**
* Definitions of feature flags.
@@ -245,7 +247,11 @@ public class Flags {
String modificationEffect,
FetchVector.Dimension[] dimensions) {
FlagId id = new FlagId(flagId);
- FetchVector vector = new FetchVector().with(HOSTNAME, Defaults.getDefaults().vespaHostname());
+ FetchVector vector = new FetchVector()
+ .with(HOSTNAME, Defaults.getDefaults().vespaHostname())
+ // Warning: In unit tests and outside official Vespa releases, the currentVersion is e.g. 7.0.0
+ // (determined by the current major version). Consider not setting VESPA_VERSION if minor = micro = 0.
+ .with(VESPA_VERSION, Vtag.currentVersion.toFullString());
U unboundFlag = factory.create(id, defaultValue, vector);
FlagDefinition definition = new FlagDefinition(unboundFlag, description, modificationEffect, dimensions);
flags.put(id, definition);
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/BlacklistCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/BlacklistCondition.java
new file mode 100644
index 00000000000..22f907a7e0d
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/BlacklistCondition.java
@@ -0,0 +1,11 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+/**
+ * @author hakonhall
+ */
+public class BlacklistCondition extends ListCondition {
+ public BlacklistCondition(CreateParams params) {
+ super(Type.BLACKLIST, params);
+ }
+}
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 a0ad08fb0b3..11d0fbbfb97 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
@@ -1,62 +1,72 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.flags.json;
import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.json.wire.WireCondition;
-import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.function.Predicate;
/**
* @author hakonhall
*/
-public class Condition implements Predicate<FetchVector> {
- public enum Type { WHITELIST, BLACKLIST }
+public interface Condition extends Predicate<FetchVector> {
+ enum Type {
+ WHITELIST,
+ BLACKLIST,
+ RELATIONAL;
- private final Type type;
- private final FetchVector.Dimension dimension;
- private final List<String> values;
+ public String toWire() { return name().toLowerCase(); }
- public Condition(Type type, FetchVector.Dimension dimension, String... values) {
- this(type, dimension, Arrays.asList(values));
- }
+ public static Type fromWire(String typeString) {
+ for (Type type : values()) {
+ if (type.name().toLowerCase().equals(typeString.toLowerCase())) {
+ return type;
+ }
+ }
- public Condition(Type type, FetchVector.Dimension dimension, List<String> values) {
- this.type = type;
- this.dimension = dimension;
- this.values = values;
+ throw new IllegalArgumentException("Unknown type: '" + typeString + "'");
+ }
}
- @Override
- public boolean test(FetchVector vector) {
- boolean isMember = vector.getValue(dimension).filter(values::contains).isPresent();
+ class CreateParams {
+ private final FetchVector.Dimension dimension;
+ private final List<String> values;
+ private final Optional<String> predicate;
- switch (type) {
- case WHITELIST: return isMember;
- case BLACKLIST: return !isMember;
- default: throw new IllegalArgumentException("Unknown type " + type);
+ public CreateParams(FetchVector.Dimension dimension, List<String> values, Optional<String> predicate) {
+ this.dimension = Objects.requireNonNull(dimension);
+ this.values = Objects.requireNonNull(values);
+ this.predicate = Objects.requireNonNull(predicate);
}
+
+ public FetchVector.Dimension dimension() { return dimension; }
+ public List<String> values() { return values; }
+ public Optional<String> predicate() { return predicate; }
}
- public static Condition fromWire(WireCondition wireCondition) {
+ static Condition fromWire(WireCondition wireCondition) {
Objects.requireNonNull(wireCondition.type);
- Type type = Type.valueOf(wireCondition.type.toUpperCase());
+ Condition.Type type = Condition.Type.fromWire(wireCondition.type);
Objects.requireNonNull(wireCondition.dimension);
FetchVector.Dimension dimension = DimensionHelper.fromWire(wireCondition.dimension);
List<String> values = wireCondition.values == null ? List.of() : wireCondition.values;
+ Optional<String> predicate = Optional.ofNullable(wireCondition.predicate);
- return new Condition(type, dimension, values);
- }
+ var params = new CreateParams(dimension, values, predicate);
- public WireCondition toWire() {
- WireCondition wire = new WireCondition();
- wire.type = type.name().toLowerCase();
- wire.dimension = DimensionHelper.toWire(dimension);
- wire.values = values.isEmpty() ? null : values;
- return wire;
+ switch (type) {
+ case WHITELIST: return new WhitelistCondition(params);
+ case BLACKLIST: return new BlacklistCondition(params);
+ case RELATIONAL: return RelationalCondition.create(params);
+ }
+
+ throw new IllegalArgumentException("Unknown type '" + type + "'");
}
+
+ WireCondition toWire();
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java
index 4fe27e81f2b..c7081ca72ab 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java
@@ -18,6 +18,7 @@ public class DimensionHelper {
serializedDimensions.put(FetchVector.Dimension.APPLICATION_ID, "application");
serializedDimensions.put(FetchVector.Dimension.NODE_TYPE, "node-type");
serializedDimensions.put(FetchVector.Dimension.CLUSTER_TYPE, "cluster-type");
+ serializedDimensions.put(FetchVector.Dimension.VESPA_VERSION, "vespa-version");
if (serializedDimensions.size() != FetchVector.Dimension.values().length) {
throw new IllegalStateException(FetchVectorHelper.class.getName() + " is not in sync with " +
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
new file mode 100644
index 00000000000..948f1fc6c3e
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java
@@ -0,0 +1,39 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.json.wire.WireCondition;
+
+import java.util.List;
+
+/**
+ * @author hakonhall
+ */
+public abstract class ListCondition implements Condition {
+ private final Condition.Type type;
+ private final FetchVector.Dimension dimension;
+ private final List<String> values;
+ private final boolean isWhitelist;
+
+ protected ListCondition(Type type, CreateParams params) {
+ this.type = type;
+ this.dimension = params.dimension();
+ this.values = List.copyOf(params.values());
+ this.isWhitelist = type == Type.WHITELIST;
+ }
+
+ @Override
+ public boolean test(FetchVector fetchVector) {
+ boolean listContainsValue = fetchVector.getValue(dimension).map(values::contains).orElse(false);
+ return isWhitelist == listContainsValue;
+ }
+
+ @Override
+ public WireCondition toWire() {
+ var condition = new WireCondition();
+ condition.type = type.toWire();
+ condition.dimension = DimensionHelper.toWire(dimension);
+ condition.values = values.isEmpty() ? null : values;
+ return condition;
+ }
+}
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
new file mode 100644
index 00000000000..996f1f925f7
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java
@@ -0,0 +1,58 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.component.Version;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.json.wire.WireCondition;
+
+import java.util.function.Predicate;
+
+/**
+ * @author hakonhall
+ */
+public class RelationalCondition implements Condition {
+ private final RelationalPredicate relationalPredicate;
+ private final Predicate<String> predicate;
+ private final FetchVector.Dimension dimension;
+
+ public static RelationalCondition create(CreateParams params) {
+ String predicate = params.predicate().orElseThrow(() ->
+ new IllegalArgumentException(RelationalCondition.class.getSimpleName() +
+ " requires the predicate field in the condition"));
+ RelationalPredicate relationalPredicate = RelationalPredicate.fromWire(predicate);
+
+ switch (params.dimension()) {
+ case VESPA_VERSION:
+ final Version rightVersion = Version.fromString(relationalPredicate.rightOperand());
+ Predicate<String> p = (String leftString) -> {
+ Version leftVersion = Version.fromString(leftString);
+ return relationalPredicate.operator().evaluate(leftVersion, rightVersion);
+ };
+ return new RelationalCondition(relationalPredicate, p, params.dimension());
+ default:
+ throw new IllegalArgumentException(RelationalCondition.class.getSimpleName() +
+ " not supported for dimension " + FetchVector.Dimension.VESPA_VERSION.name());
+ }
+ }
+
+ private RelationalCondition(RelationalPredicate relationalPredicate, Predicate<String> predicate,
+ FetchVector.Dimension dimension) {
+ this.relationalPredicate = relationalPredicate;
+ this.predicate = predicate;
+ this.dimension = dimension;
+ }
+
+ @Override
+ public boolean test(FetchVector fetchVector) {
+ return fetchVector.getValue(dimension).map(predicate::test).orElse(false);
+ }
+
+ @Override
+ public WireCondition toWire() {
+ var condition = new WireCondition();
+ condition.type = Condition.Type.RELATIONAL.toWire();
+ condition.dimension = DimensionHelper.toWire(dimension);
+ condition.predicate = relationalPredicate.toWire();
+ return condition;
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalOperator.java b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalOperator.java
new file mode 100644
index 00000000000..ca7a997f447
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalOperator.java
@@ -0,0 +1,39 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * @author hakonhall
+ */
+public enum RelationalOperator {
+ EQUAL ("==", compareToValue -> compareToValue == 0),
+ NOT_EQUAL ("!=", compareToValue -> compareToValue != 0),
+ LESS_EQUAL ("<=", compareToValue -> compareToValue <= 0),
+ LESS ("<" , compareToValue -> compareToValue < 0),
+ GREATER_EQUAL(">=", compareToValue -> compareToValue >= 0),
+ GREATER (">" , compareToValue -> compareToValue > 0);
+
+ private String text;
+ private final Function<Integer, Boolean> compareToValuePredicate;
+
+ RelationalOperator(String text, Function<Integer, Boolean> compareToValuePredicate) {
+ this.text = text;
+ this.compareToValuePredicate = compareToValuePredicate;
+ }
+
+ public String toText() { return text; }
+
+ /** Returns true if 'left op right' is true, with 'op' being the operator represented by this. */
+ public <T extends Comparable<T>> boolean evaluate(T left, T right) {
+ Objects.requireNonNull(left);
+ Objects.requireNonNull(right);
+ return evaluate(left.compareTo(right));
+ }
+
+ public boolean evaluate(int compareToValue) {
+ return compareToValuePredicate.apply(compareToValue);
+ }
+}
+
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalPredicate.java b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalPredicate.java
new file mode 100644
index 00000000000..c5ad195e0d2
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalPredicate.java
@@ -0,0 +1,43 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author hakonhall
+ */
+public class RelationalPredicate {
+ private final String originalPredicateString;
+ private final RelationalOperator operator;
+ private final String rightOperand;
+
+ /** @param predicateString is e.g. "&gt; SUFFIX" or "&lt;=SUFFIX". The first part is {@link RelationalOperator}. */
+ public static RelationalPredicate fromWire(String predicateString) {
+ // Make sure we try to match e.g. "<=" before "<" as the prefix of predicateString.
+ List<RelationalOperator> operatorsByDecendingLength = Stream.of(RelationalOperator.values())
+ .sorted(Comparator.comparing(operator -> - operator.toText().length()))
+ .collect(Collectors.toList());
+
+ for (var operator : operatorsByDecendingLength) {
+ if (predicateString.startsWith(operator.toText())) {
+ String suffix = predicateString.substring(operator.toText().length());
+ return new RelationalPredicate(predicateString, operator, suffix);
+ }
+ }
+
+ throw new IllegalArgumentException("Predicate string '" + predicateString + "' does not start with a relation operator");
+ }
+
+ private RelationalPredicate(String originalPredicateString, RelationalOperator operator, String rightOperand) {
+ this.originalPredicateString = originalPredicateString;
+ this.operator = operator;
+ this.rightOperand = rightOperand;
+ }
+
+ public RelationalOperator operator() { return operator; }
+ public String rightOperand() { return rightOperand; }
+ public String toWire() { return originalPredicateString; }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/WhitelistCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/WhitelistCondition.java
new file mode 100644
index 00000000000..5c3f8bd3ade
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/WhitelistCondition.java
@@ -0,0 +1,11 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+/**
+ * @author hakonhall
+ */
+public class WhitelistCondition extends ListCondition {
+ public WhitelistCondition(CreateParams params) {
+ super(Type.WHITELIST, params);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java
index 2020ce1e49f..1729444fcf2 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java
@@ -16,4 +16,5 @@ public class WireCondition {
@JsonProperty("type") public String type;
@JsonProperty("dimension") public String dimension;
@JsonProperty("values") public List<String> values;
+ @JsonProperty("predicate") public String predicate;
}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
index d19442ae0f0..cf8d06fd312 100644
--- a/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
@@ -4,9 +4,10 @@ package com.yahoo.vespa.flags.json;
import com.yahoo.vespa.flags.FetchVector;
import org.junit.Test;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.List;
+import java.util.Optional;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -17,8 +18,8 @@ public class ConditionTest {
@Test
public void testWhitelist() {
String hostname1 = "host1";
- Condition condition = new Condition(Condition.Type.WHITELIST, FetchVector.Dimension.HOSTNAME,
- Stream.of(hostname1).collect(Collectors.toList()));
+ var params = new Condition.CreateParams(FetchVector.Dimension.HOSTNAME, List.of(hostname1), Optional.empty());
+ Condition condition = new WhitelistCondition(params);
assertFalse(condition.test(new FetchVector()));
assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
@@ -28,11 +29,38 @@ public class ConditionTest {
@Test
public void testBlacklist() {
String hostname1 = "host1";
- Condition condition = new Condition(Condition.Type.BLACKLIST, FetchVector.Dimension.HOSTNAME,
- Stream.of(hostname1).collect(Collectors.toList()));
+ var params = new Condition.CreateParams(FetchVector.Dimension.HOSTNAME, List.of(hostname1), Optional.empty());
+ Condition condition = new BlacklistCondition(params);
assertTrue(condition.test(new FetchVector()));
assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, hostname1)));
}
+
+ @Test
+ public void testRelational() {
+ verifyVespaVersionFor("<", true, false, false);
+ verifyVespaVersionFor("<=", true, true, false);
+ verifyVespaVersionFor(">", false, false, true);
+ verifyVespaVersionFor(">=", false, true, true);
+
+ // Test with empty fetch vector along vespa version dimension (this should never happen as the
+ // version is always available through Vtag, although Vtag has a dummy version number for e.g.
+ // locally run unit tests that hasn't set the release Vespa version).
+ var params = new Condition.CreateParams(FetchVector.Dimension.VESPA_VERSION, List.of(), Optional.of(">=7.1.2"));
+ Condition condition = RelationalCondition.create(params);
+ assertFalse(condition.test(new FetchVector()));
+ }
+
+ private void verifyVespaVersionFor(String operator, boolean whenLess, boolean whenEqual, boolean whenGreater) {
+ assertEquals(whenLess, vespaVersionCondition("7.2.4", operator + "7.3.4"));
+ assertEquals(whenEqual, vespaVersionCondition("7.3.4", operator + "7.3.4"));
+ assertEquals(whenGreater, vespaVersionCondition("7.4.4", operator + "7.3.4"));
+ }
+
+ private boolean vespaVersionCondition(String vespaVersion, String predicate) {
+ var params = new Condition.CreateParams(FetchVector.Dimension.VESPA_VERSION, List.of(), Optional.of(predicate));
+ Condition condition = RelationalCondition.create(params);
+ return condition.test(new FetchVector().with(FetchVector.Dimension.VESPA_VERSION, vespaVersion));
+ }
}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
index b0e4cd0f682..8326b14fcbf 100644
--- a/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
@@ -48,6 +48,11 @@ public class SerializationTest {
" \"type\": \"blacklist\",\n" +
" \"dimension\": \"hostname\",\n" +
" \"values\": [ \"h1\" ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"relational\",\n" +
+ " \"dimension\": \"vespa-version\",\n" +
+ " \"predicate\": \">=7.3.4\"\n" +
" }\n" +
" ],\n" +
" \"value\": true\n" +
@@ -66,7 +71,7 @@ public class SerializationTest {
assertThat(wireData.id, equalTo("id2"));
// rule
assertThat(wireData.rules.size(), equalTo(1));
- assertThat(wireData.rules.get(0).andConditions.size(), equalTo(2));
+ assertThat(wireData.rules.get(0).andConditions.size(), equalTo(3));
assertThat(wireData.rules.get(0).value.getNodeType(), equalTo(JsonNodeType.BOOLEAN));
assertThat(wireData.rules.get(0).value.asBoolean(), equalTo(true));
// first condition
@@ -79,6 +84,11 @@ public class SerializationTest {
assertThat(blacklistCondition.type, equalTo("blacklist"));
assertThat(blacklistCondition.dimension, equalTo("hostname"));
assertThat(blacklistCondition.values, equalTo(List.of("h1")));
+ // third condition
+ WireCondition relationalCondition = wireData.rules.get(0).andConditions.get(2);
+ assertThat(relationalCondition.type, equalTo("relational"));
+ assertThat(relationalCondition.dimension, equalTo("vespa-version"));
+ assertThat(relationalCondition.predicate, equalTo(">=7.3.4"));
// attributes
assertThat(wireData.defaultFetchVector, notNullValue());