All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.config.application.api.ValidationOverrides Maven / Gradle / Ivy

There is a newer version: 8.441.21
Show newest version
// 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