diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2017-06-15 12:13:18 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2017-06-15 12:13:18 +0200 |
commit | e5a1f22a814923e4f6a2a298a76630d6567e02b8 (patch) | |
tree | 9798fa6a74dedd3978a7984caa748756ffe71dc5 /config-model-api | |
parent | 716b933c92f020ac93c340cccbd666751e307d81 (diff) |
Make ValidationOverrides available from the outside
Diffstat (limited to 'config-model-api')
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) { + } + } + +} |