summaryrefslogtreecommitdiffstats
path: root/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstancesSpec.java
blob: 344f34388047ad5027f4d60c936c80de3f9b858b (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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// Copyright 2019 Oath Inc. 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.provision.AthenzDomain;
import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * The deployment spec for some specified application instances
 *
 * @author bratseth
 */
public class DeploymentInstancesSpec extends DeploymentSpec.Step {

    /** The instances deployed in this step */
    private final List<InstanceName> ids;

    private final List<DeploymentSpec.Step> steps;
    private final DeploymentSpec.UpgradePolicy upgradePolicy;
    private final List<DeploymentSpec.ChangeBlocker> changeBlockers;
    private final Optional<String> globalServiceId;
    private final Optional<AthenzDomain> athenzDomain;
    private final Optional<AthenzService> athenzService;
    private final Notifications notifications;
    private final List<Endpoint> endpoints;

    public DeploymentInstancesSpec(List<InstanceName> ids,
                                   List<DeploymentSpec.Step> steps,
                                   DeploymentSpec.UpgradePolicy upgradePolicy,
                                   List<DeploymentSpec.ChangeBlocker> changeBlockers,
                                   Optional<String> globalServiceId,
                                   Optional<AthenzDomain> athenzDomain,
                                   Optional<AthenzService> athenzService,
                                   Notifications notifications,
                                   List<Endpoint> endpoints) {
        this.ids = List.copyOf(ids);
        this.steps = List.copyOf(completeSteps(new ArrayList<>(steps)));
        this.upgradePolicy = upgradePolicy;
        this.changeBlockers = changeBlockers;
        this.globalServiceId = globalServiceId;
        this.athenzDomain = athenzDomain;
        this.athenzService = athenzService;
        this.notifications = notifications;
        this.endpoints = List.copyOf(validateEndpoints(endpoints, this.steps));
        validateZones(this.steps);
        validateEndpoints(this.steps, globalServiceId, this.endpoints);
        validateAthenz();
    }

    public List<InstanceName> names() { return ids; }

    /** Adds missing required steps and reorders steps to a permissible order */
    private static List<DeploymentSpec.Step> completeSteps(List<DeploymentSpec.Step> steps) {
        // Add staging if required and missing
        if (steps.stream().anyMatch(step -> step.deploysTo(Environment.prod)) &&
            steps.stream().noneMatch(step -> step.deploysTo(Environment.staging))) {
            steps.add(new DeploymentSpec.DeclaredZone(Environment.staging));
        }

        // Add test if required and missing
        if (steps.stream().anyMatch(step -> step.deploysTo(Environment.staging)) &&
            steps.stream().noneMatch(step -> step.deploysTo(Environment.test))) {
            steps.add(new DeploymentSpec.DeclaredZone(Environment.test));
        }

        // Enforce order test, staging, prod
        DeploymentSpec.DeclaredZone testStep = remove(Environment.test, steps);
        if (testStep != null)
            steps.add(0, testStep);
        DeploymentSpec.DeclaredZone stagingStep = remove(Environment.staging, steps);
        if (stagingStep != null)
            steps.add(1, stagingStep);

        return steps;
    }

    /**
     * Removes the first occurrence of a deployment step to the given environment and returns it.
     *
     * @return the removed step, or null if it is not present
     */
    private static DeploymentSpec.DeclaredZone remove(Environment environment, List<DeploymentSpec.Step> steps) {
        for (int i = 0; i < steps.size(); i++) {
            if (steps.get(i).deploysTo(environment))
                return (DeploymentSpec.DeclaredZone)steps.remove(i);
        }
        return null;
    }

    /** Throw an IllegalArgumentException if any production zone is declared multiple times */
    private void validateZones(List<DeploymentSpec.Step> steps) {
        Set<DeploymentSpec.DeclaredZone> zones = new HashSet<>();

        for (DeploymentSpec.Step step : steps)
            for (DeploymentSpec.DeclaredZone zone : step.zones())
                ensureUnique(zone, zones);
    }

    private void ensureUnique(DeploymentSpec.DeclaredZone zone, Set<DeploymentSpec.DeclaredZone> zones) {
        if ( ! zones.add(zone))
            throw new IllegalArgumentException(zone + " is listed twice in deployment.xml");
    }

    /** Validates the endpoints and makes sure default values are respected */
    private List<Endpoint> validateEndpoints(List<Endpoint> endpoints, List<DeploymentSpec.Step> steps) {
        Objects.requireNonNull(endpoints, "Missing endpoints parameter");

        var productionRegions = steps.stream()
                                     .filter(step -> step.deploysTo(Environment.prod))
                                     .flatMap(step -> step.zones().stream())
                                     .flatMap(zone -> zone.region().stream())
                                     .map(RegionName::value)
                                     .collect(Collectors.toSet());

        var rebuiltEndpointsList = new ArrayList<Endpoint>();

        for (var endpoint : endpoints) {
            if (endpoint.regions().isEmpty()) {
                var rebuiltEndpoint = endpoint.withRegions(productionRegions);
                rebuiltEndpointsList.add(rebuiltEndpoint);
            } else {
                rebuiltEndpointsList.add(endpoint);
            }
        }

        return List.copyOf(rebuiltEndpointsList);
    }

    /** Throw an IllegalArgumentException if an endpoint refers to a region that is not declared in 'prod' */
    private void validateEndpoints(List<DeploymentSpec.Step> steps, Optional<String> globalServiceId, List<Endpoint> endpoints) {
        if (globalServiceId.isPresent() && ! endpoints.isEmpty()) {
            throw new IllegalArgumentException("Providing both 'endpoints' and 'global-service-id'. Use only 'endpoints'.");
        }

        var stepZones = steps.stream()
                             .flatMap(s -> s.zones().stream())
                             .flatMap(z -> z.region().stream())
                             .collect(Collectors.toSet());

        for (var endpoint : endpoints){
            for (var endpointRegion : endpoint.regions()) {
                if (! stepZones.contains(endpointRegion)) {
                    throw new IllegalArgumentException("Region used in endpoint that is not declared in 'prod': " + endpointRegion);
                }
            }
        }
    }

    /**
     * Throw an IllegalArgumentException if Athenz configuration violates:
     * domain not configured -> no zone can configure service
     * domain configured -> all zones must configure service
     */
    private void validateAthenz() {
        // If athenz domain is not set, athenz service cannot be set on any level
        if (athenzDomain.isEmpty()) {
            for (DeploymentSpec.DeclaredZone zone : zones()) {
                if(zone.athenzService().isPresent()) {
                    throw new IllegalArgumentException("Athenz service configured for zone: " + zone + ", but Athenz domain is not configured");
                }
            }
            // if athenz domain is not set, athenz service must be set implicitly or directly on all zones.
        } else if (athenzService.isEmpty()) {
            for (DeploymentSpec.DeclaredZone zone : zones()) {
                if (zone.athenzService().isEmpty()) {
                    throw new IllegalArgumentException("Athenz domain is configured, but Athenz service not configured for zone: " + zone);
                }
            }
        }
    }

    @Override
    public Duration delay() {
        return Duration.ofSeconds(steps.stream().mapToLong(step -> (step.delay().getSeconds())).sum());
    }

    /** Returns the deployment steps inside this in the order they will be performed */
    public List<DeploymentSpec.Step> steps() { return steps; }

    /** Returns the upgrade policy of this, which is defaultPolicy if none is specified */
    public DeploymentSpec.UpgradePolicy upgradePolicy() { return upgradePolicy; }

    /** Returns time windows where upgrades are disallowed for these instances */
    public List<DeploymentSpec.ChangeBlocker> changeBlocker() { return changeBlockers; }

    /** Returns the ID of the service to expose through global routing, if present */
    public Optional<String> globalServiceId() { return globalServiceId; }

    /** Returns whether the instances in this step can upgrade at the given instant */
    public boolean canUpgradeAt(Instant instant) {
        return changeBlockers.stream().filter(block -> block.blocksVersions())
                                      .noneMatch(block -> block.window().includes(instant));
    }

    /** Returns whether an application revision change for these instances can occur at the given instant */
    public boolean canChangeRevisionAt(Instant instant) {
        return changeBlockers.stream().filter(block -> block.blocksRevisions())
                             .noneMatch(block -> block.window().includes(instant));
    }

    /** Returns all the DeclaredZone deployment steps in the order they are declared */
    public List<DeploymentSpec.DeclaredZone> zones() {
        return steps.stream()
                    .flatMap(step -> step.zones().stream())
                    .collect(Collectors.toList());
    }

    /** Returns whether this deployment spec specifies the given zone, either implicitly or explicitly */
    @Override
    public boolean deploysTo(Environment environment, Optional<RegionName> region) {
        for (DeploymentSpec.Step step : steps)
            if (step.deploysTo(environment, region)) return true;
        return false;
    }

    /** Returns the athenz domain if configured */
    public Optional<AthenzDomain> athenzDomain() { return athenzDomain; }

    /** Returns the athenz service for environment/region if configured */
    public Optional<AthenzService> athenzService(Environment environment, RegionName region) {
        AthenzService athenzService = zones().stream()
                                             .filter(zone -> zone.deploysTo(environment, Optional.of(region)))
                                             .findFirst()
                                             .flatMap(DeploymentSpec.DeclaredZone::athenzService)
                                             .orElse(this.athenzService.orElse(null));
        return Optional.ofNullable(athenzService);
    }

    /** Returns the notification configuration of these instances */
    public Notifications notifications() { return notifications; }

    /** Returns the rotations configuration of these instances */
    public List<Endpoint> endpoints() { return endpoints; }

    /** Returns whether this instances deployment specifies the given zone, either implicitly or explicitly */
    public boolean includes(Environment environment, Optional<RegionName> region) {
        for (DeploymentSpec.Step step : steps)
            if (step.deploysTo(environment, region)) return true;
        return false;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DeploymentInstancesSpec other = (DeploymentInstancesSpec) o;
        return globalServiceId.equals(other.globalServiceId) &&
               upgradePolicy == other.upgradePolicy &&
               changeBlockers.equals(other.changeBlockers) &&
               steps.equals(other.steps) &&
               athenzDomain.equals(other.athenzDomain) &&
               athenzService.equals(other.athenzService) &&
               notifications.equals(other.notifications) &&
               endpoints.equals(other.endpoints);
    }

    @Override
    public int hashCode() {
        return Objects.hash(globalServiceId, upgradePolicy, changeBlockers, steps, athenzDomain, athenzService, notifications, endpoints);
    }

    @Override
    public String toString() {
        return "instance" + ( ids.size() < 1 ? " " : "s " ) + ids;
    }

}