com.yahoo.config.application.api.ValidationOverrides Maven / Gradle / Ivy
// Copyright Vespa.ai. 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.io.IOUtils;
import com.yahoo.text.XML;
import org.w3c.dom.Element;
import java.io.IOException;
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.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* 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 {
public static final ValidationOverrides empty = new ValidationOverrides(List.of(), " ");
private final List overrides;
private final String xmlForm;
/** Creates a validation overrides which does not have an XML form */
public ValidationOverrides(List overrides) {
this(overrides, null);
}
private ValidationOverrides(List overrides, String xmlForm) {
this.overrides = List.copyOf(overrides);
this.xmlForm = xmlForm;
}
/** Throws a ValidationException unless all given validation is overridden at this time */
public void invalid(Map> messagesByValidationId, Instant now) {
invalidException(messagesByValidationId, now).ifPresent(e -> { throw e; });
}
/** Throws a ValidationException unless this validation is overridden at this time */
public void invalid(ValidationId validationId, String message, Instant now) {
if ( ! allows(validationId, now))
throw new ValidationException(validationId, message);
}
public Optional invalidException(Map> messagesByValidationId, Instant now) {
Map> disallowed = new HashMap<>(messagesByValidationId);
disallowed.keySet().removeIf(id -> allows(id, now));
if (disallowed.size() == 1 && disallowed.values().iterator().next().size() == 1) // Single-message form if possible.
return Optional.of(new ValidationException(disallowed.keySet().iterator().next(),
disallowed.values().iterator().next().iterator().next()));
if ( ! disallowed.isEmpty())
return Optional.of(new ValidationException(disallowed));
return Optional.empty();
}
/** Returns whether the given (assumed invalid) change is allowed by this at the moment */
public boolean allows(ValidationId validationId, Instant now) {
for (Allow override : overrides) {
if (override.allows(validationId, now))
return true;
}
return false;
}
/** Validates overrides (checks 'until' date') */
public void validate(Instant now, Consumer reporter) {
for (Allow override : overrides) {
if (now.plus(Duration.ofDays(30)).isBefore(override.until))
reporter.accept("validation-overrides is invalid: " + override +
" is too far in the future: Max 30 days is allowed");
}
}
/** Validates overrides (checks 'until' date') */
public boolean validate(Instant now) {
validate(now, message -> { throw new IllegalArgumentException(message); });
return false;
}
/** Returns the XML form of this, or null if it was not created by fromXml, nor is empty */
public String xmlForm() { return xmlForm; }
public static String toAllowMessage(ValidationId id) {
return "To allow this add " + id + " to validation-overrides.xml" +
", see https://docs.vespa.ai/en/reference/validation-overrides.html";
}
/**
* Returns a ValidationOverrides instance with the content of the given Reader.
*
* @param reader the reader containing a validation-overrides XML structure
* @return a ValidationOverrides from the argument
* @throws IllegalArgumentException if the validation-allows.xml file exists but is invalid
*/
public static ValidationOverrides fromXml(Reader reader) {
try {
return fromXml(IOUtils.readAll(reader));
} catch (IOException e) {
throw new IllegalArgumentException("Could not read validation-overrides", e);
}
}
/**
* Returns a ValidationOverrides instance with the content of the given XML string.
* An empty ValidationOverrides is returned if the argument is empty.
*
* @param xmlForm the string which optionally contains a validation-overrides XML structure
* @return a ValidationOverrides from the argument
* @throws IllegalArgumentException if the validation-allows.xml file exists but is invalid
*/
public static ValidationOverrides fromXml(String xmlForm) {
if ( xmlForm.isEmpty()) return ValidationOverrides.empty;
// Assume valid structure is ensured by schema validation
Element root = XML.getDocument(xmlForm).getDocumentElement();
List overrides = new ArrayList<>();
for (Element allow : XML.getChildren(root, "allow")) {
try {
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.from(XML.getValue(allow));
// skip unknown ids as they may be valid for other model versions
validationId.ifPresent(id -> overrides.add(new Allow(id, until)));
} catch (RuntimeException e) {
throw new IllegalArgumentException(e);
}
}
return new ValidationOverrides(overrides, xmlForm);
}
/** 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.
*/
public static class ValidationException extends IllegalArgumentException {
private final Map> messagesById = new LinkedHashMap<>();
static final long serialVersionUID = 789984668;
private ValidationException(ValidationId validationId, String message) {
super(validationId + ": " + message + ". " + toAllowMessage(validationId));
messagesById.put(validationId, List.of(message));
}
private ValidationException(Map> messagesById) {
super(messagesById.entrySet().stream()
.map(messages -> messages.getKey() + ":\n\t" +
String.join("\n\t", messages.getValue()) + "\n" +
toAllowMessage(messages.getKey()))
.collect(Collectors.joining("\n")));
messagesById.forEach((id, messages) -> this.messagesById.put(id, List.copyOf(messages)));
}
public Map> messagesById() { return Map.copyOf(messagesById); }
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy