// 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.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.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 {
public static final ValidationOverrides empty = new ValidationOverrides(ImmutableList.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 = ImmutableList.copyOf(overrides);
this.xmlForm = xmlForm;
}
/** 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 boolean allows(String validationIdString, Instant now) {
Optional validationId = ValidationId.from(validationIdString);
if ( ! validationId.isPresent()) return false; // unknown id -> not allowed
return allows(validationId.get(), now);
}
/** 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 (now.plus(Duration.ofDays(30)).isBefore(override.until))
throw new IllegalArgumentException(override + " is too far in the future: Max 30 days is allowed");
if (override.allows(validationId, now))
return true;
}
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; }
/**
* 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 deployment spec", 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;
try {
// 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")) {
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));
if (validationId.isPresent()) // skip unknown ids as they may be valid for other model versions
overrides.add(new ValidationOverrides.Allow(validationId.get(), until));
}
return new ValidationOverrides(overrides, xmlForm);
}
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(); }
}
}