summaryrefslogtreecommitdiffstats
path: root/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java
blob: 11f9add6b25e940f6d2a94fa10059c47ca7bff38 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// 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(), "<deployment version='1.0'/>");

    private final List<Allow> overrides;

    private final String xmlForm;

    /** Creates a validation overrides which does not have an xml form */
    public ValidationOverrides(List<Allow> overrides) {
        this(overrides, null);
    }

    private ValidationOverrides(List<Allow> 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 = 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<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 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(); }

    }

}