summaryrefslogtreecommitdiffstats
path: root/config-model-api
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2017-06-15 12:13:18 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2017-06-15 12:13:18 +0200
commite5a1f22a814923e4f6a2a298a76630d6567e02b8 (patch)
tree9798fa6a74dedd3978a7984caa748756ffe71dc5 /config-model-api
parent716b933c92f020ac93c340cccbd666751e307d81 (diff)
Make ValidationOverrides available from the outside
Diffstat (limited to 'config-model-api')
-rw-r--r--config-model-api/pom.xml6
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java45
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java144
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java90
4 files changed, 285 insertions, 0 deletions
diff --git a/config-model-api/pom.xml b/config-model-api/pom.xml
index fd62a5c4c2f..6618612df31 100644
--- a/config-model-api/pom.xml
+++ b/config-model-api/pom.xml
@@ -63,6 +63,12 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>testutil</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>17.0</version>
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
new file mode 100644
index 00000000000..37cb6e40f3d
--- /dev/null
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
@@ -0,0 +1,45 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.application.api;
+
+import java.util.Optional;
+
+/**
+ * Ids of validations that can be overridden
+ *
+ * @author bratseth
+ */
+public enum ValidationId {
+
+ indexingChange("indexing-change"), // Changing what tokens are expected and stored in field indexes
+ indexModeChange("indexing-mode-change"), // Changing the index mode (streaming, indexed, store-only) of documents
+ fieldTypeChange("field-type-change"), // Field type changes
+ clusterSizeReduction("cluster-size-reduction"), // Large reductions in cluster size
+ contentClusterRemoval("content-cluster-removal"), // Removal (or id change) of content clusters
+ deploymentRemoval("deployment-removal"), // Removal of production zones from deployment.xml
+ skipAutomaticTenantUpgradeTests("skip-automatic-tenant-upgrade-test"), // Skip platform supplied staging tests
+ configModelVersionMismatch("config-model-version-mismatch"), // Internal use
+ skipOldConfigModels("skip-old-config-models"), // Internal use
+ forceAutomaticTenantUpgradeTests("force-automatic-tenant-upgrade-test"); // Internal use
+
+ private final String id;
+
+ ValidationId(String id) { this.id = id; }
+
+ public String value() { return id; }
+
+ @Override
+ public String toString() { return id; }
+
+ /**
+ * Returns the validation id from this string.
+ * Use this instead of valueOf to match string on the (canonical) dash-separated form.
+ *
+ * @return the matching validation id or empty if none
+ */
+ public static Optional<ValidationId> from(String id) {
+ for (ValidationId candidate : ValidationId.values())
+ if (id.equals(candidate.toString())) return Optional.of(candidate);
+ return Optional.empty();
+ }
+
+}
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java
new file mode 100644
index 00000000000..9fd2b7f2bf8
--- /dev/null
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java
@@ -0,0 +1,144 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.application.api;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.text.XML;
+import org.w3c.dom.Element;
+
+import java.io.Reader;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A set of allows which suppresses specific validations in limited time periods.
+ * This is useful to be able to complete a deployment in cases where the application
+ * owner believes that the changes to be deployed have acceptable consequences.
+ * Immutable.
+ *
+ * @author bratseth
+ */
+public class ValidationOverrides {
+
+ private final List<Allow> overrides;
+
+ /** Instant to use as "now". This is a field to allow unit testing. */
+ private final Instant now;
+
+ /** Creates validation overrides for the current instant */
+ public ValidationOverrides(List<Allow> overrides) {
+ this(overrides, Instant.now());
+ }
+
+ public ValidationOverrides(List<Allow> overrides, Instant now) {
+ this.overrides = ImmutableList.copyOf(overrides);
+ this.now = now;
+ for (Allow override : overrides)
+ if (now.plus(Duration.ofDays(30)).isBefore(override.until))
+ throw new IllegalArgumentException(override + " is too far in the future: Max 30 days is allowed");
+ }
+
+ /** Throws a ValidationException unless this validation is overridden at this time */
+ public void invalid(ValidationId validationId, String message) {
+ if ( ! allows(validationId))
+ throw new ValidationException(validationId, message);
+ }
+
+ public boolean allows(String validationIdString) {
+ Optional<ValidationId> validationId = ValidationId.from(validationIdString);
+ if ( ! validationId.isPresent()) return false; // unknown id -> not allowed
+ return allows(validationId.get());
+ }
+
+ /** Returns whether the given (assumed invalid) change is allowed by this at the moment */
+ public boolean allows(ValidationId validationId) {
+ for (Allow override : overrides)
+ if (override.allows(validationId, now))
+ return true;
+ return false;
+ }
+
+ public static ValidationOverrides empty() { return new ValidationOverrides(ImmutableList.of()); }
+
+ /**
+ * Returns a ValidationOverrides instance with the content of the given Reader.
+ * An empty ValidationOverrides is returned if the argument is empty.
+ *
+ * @param reader the reader which optionally contains a validation-overrides XML structure
+ * @param now the instant to use as "now", settable for unit testing
+ * @return a ValidationOverrides from the argument
+ * @throws IllegalArgumentException if the validation-allows.xml file exists but is invalid
+ */
+ public static ValidationOverrides read(Optional<Reader> reader, Instant now) {
+ if ( ! reader.isPresent()) return ValidationOverrides.empty();
+
+ try {
+ // Assume valid structure is ensured by schema validation
+ Element root = XML.getDocument(reader.get()).getDocumentElement();
+ List<ValidationOverrides.Allow> overrides = new ArrayList<>();
+ for (Element allow : XML.getChildren(root, "allow")) {
+ Instant until = LocalDate.parse(allow.getAttribute("until"), DateTimeFormatter.ISO_DATE)
+ .atStartOfDay().atZone(ZoneOffset.UTC).toInstant()
+ .plus(Duration.ofDays(1)); // Make the override valid *on* the "until" date
+ Optional<ValidationId> validationId = ValidationId.from(XML.getValue(allow));
+ if (validationId.isPresent()) // skip unknonw ids as they may be valid for other model versions
+ overrides.add(new ValidationOverrides.Allow(validationId.get(), until));
+ }
+ return new ValidationOverrides(overrides, now);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("validation-overrides is invalid", e);
+ }
+ }
+
+ /** A validation override which allows a particular change. Immutable. */
+ public static class Allow {
+
+ private final ValidationId validationId;
+ private final Instant until;
+
+ public Allow(ValidationId validationId, Instant until) {
+ this.validationId = validationId;
+ this.until = until;
+ }
+
+ public boolean allows(ValidationId validationId, Instant now) {
+ return this.validationId.equals(validationId) && now.isBefore(until);
+ }
+
+ @Override
+ public String toString() { return "allow '" + validationId + "' until " + until; }
+
+ }
+
+ /**
+ * A deployment validation exception.
+ * Deployment validations can be {@link ValidationOverrides overridden} based on their id.
+ * The purpose of this exception is to model that id as a separate field.
+ */
+ public static class ValidationException extends IllegalArgumentException {
+
+ static final long serialVersionUID = 789984668;
+
+ private final ValidationId validationId;
+
+ private ValidationException(ValidationId validationId, String message) {
+ super(message);
+ this.validationId = validationId;
+ }
+
+ /** Returns the unique id of this validation, which can be used to {@link ValidationOverrides override} it */
+ public ValidationId validationId() { return validationId; }
+
+ /** Returns "validationId: message" */
+ @Override
+ public String getMessage() { return validationId + ": " + super.getMessage(); }
+
+ }
+
+}
diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java
new file mode 100644
index 00000000000..2528fe8883e
--- /dev/null
+++ b/config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java
@@ -0,0 +1,90 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.application.api;
+
+import com.yahoo.config.application.api.ValidationId;
+import com.yahoo.config.application.api.ValidationOverrides;
+import com.yahoo.test.ManualClock;
+import org.junit.Assert;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Optional;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ */
+public class ValidationOverrideTest {
+
+ @Test
+ public void testValidationOverridesInIsolation() throws IOException, SAXException {
+ String validationOverrides =
+ "<validation-overrides>" +
+ " <allow until='2000-01-01'>indexing-change</allow>" +
+ " <allow until='2000-01-03' comment='any text'>indexing-mode-change</allow>" +
+ "</validation-overrides>";
+
+ {
+
+ ValidationOverrides overrides = ValidationOverrides.read(Optional.of(new StringReader(validationOverrides)),
+ ManualClock.at("2000-01-01T23:59:00"));
+ assertOverridden("indexing-change", overrides);
+ assertOverridden("indexing-mode-change", overrides);
+ assertNotOverridden("field-type-change", overrides);
+ }
+
+ {
+ ValidationOverrides overrides = ValidationOverrides.read(Optional.of(new StringReader(validationOverrides)),
+ ManualClock.at("2000-01-02T00:00:00"));
+ assertNotOverridden("indexing-change", overrides);
+ assertOverridden("indexing-mode-change", overrides);
+ assertNotOverridden("field-type-change", overrides);
+ }
+
+ {
+ ValidationOverrides overrides = ValidationOverrides.read(Optional.of(new StringReader(validationOverrides)),
+ ManualClock.at("2000-01-04T00:00:00"));
+ assertNotOverridden("indexing-change", overrides);
+ assertNotOverridden("indexing-mode-change", overrides);
+ assertNotOverridden("field-type-change", overrides);
+ }
+
+ }
+
+ @Test
+ public void testInvalidOverridePeriod() throws IOException, SAXException {
+ String validationOverrides =
+ "<validation-overrides>" +
+ " <allow until='2000-02-02'>indexing-change</allow>" +
+ "</validation-overrides>";
+
+ try {
+ ValidationOverrides.read(Optional.of(new StringReader(validationOverrides)),
+ ManualClock.at("2000-01-01T23:59:00"));
+ Assert.fail("Expected validation interval override validation validation failure");
+ }
+ catch (IllegalArgumentException e) {
+ Assert.assertEquals("validation-overrides is invalid", e.getMessage());
+ Assert.assertEquals("allow 'indexing-change' until 2000-02-03T00:00:00Z is too far in the future: Max 30 days is allowed",
+ e.getCause().getMessage());
+ }
+ }
+
+ private void assertOverridden(String validationId, ValidationOverrides overrides) {
+ overrides.invalid(ValidationId.from(validationId).get(), "message"); // should not throw exception
+ }
+
+ private void assertNotOverridden(String validationId, ValidationOverrides overrides) {
+ try {
+ overrides.invalid(ValidationId.from(validationId).get(), "message");
+ Assert.fail("Expected '" + validationId + "' to not be overridden");
+ }
+ catch (ValidationOverrides.ValidationException expected) {
+ }
+ }
+
+}