aboutsummaryrefslogtreecommitdiffstats
path: root/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java
blob: 22b32d54bfd0404600de32c5b02a10044e182411 (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
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.api.integration.deployment;

import com.yahoo.component.Version;

import java.time.Instant;
import java.util.Optional;
import java.util.OptionalLong;

import static ai.vespa.validation.Validation.requireAtLeast;
import static java.util.Objects.requireNonNull;

/**
 * An application package version, identified by a source revision and a build number.
 *
 * @author bratseth
 * @author mpolden
 * @author jonmv
 */
public class ApplicationVersion implements Comparable<ApplicationVersion> {

    // This never changes and is only used to create a valid semantic version number, as required by application bundles
    private static final String majorVersion = "1.0";

    private final RevisionId id;
    private final Optional<SourceRevision> source;
    private final Optional<String> authorEmail;
    private final Optional<Version> compileVersion;
    private final Optional<Integer> allowedMajor;
    private final Optional<Instant> buildTime;
    private final Optional<String> sourceUrl;
    private final Optional<String> commit;
    private final Optional<String> bundleHash;
    private final Optional<Instant> obsoleteAt;
    private final boolean hasPackage;
    private final boolean shouldSkip;
    private final Optional<String> description;
    private final Optional<Instant> submittedAt;
    private final int risk;

    public ApplicationVersion(RevisionId id, Optional<SourceRevision> source, Optional<String> authorEmail,
                              Optional<Version> compileVersion, Optional<Integer> allowedMajor, Optional<Instant> buildTime,
                              Optional<String> sourceUrl, Optional<String> commit, Optional<String> bundleHash,
                              Optional<Instant> obsoleteAt, boolean hasPackage, boolean shouldSkip, Optional<String> description,
                              Optional<Instant> submittedAt, int risk) {

        if (commit.isPresent() && commit.get().length() > 128)
            throw new IllegalArgumentException("Commit may not be longer than 128 characters");

        if (authorEmail.isPresent() && ! authorEmail.get().matches("[^@]+@[^@]+"))
            throw new IllegalArgumentException("Invalid author email '" + authorEmail.get() + "'.");

        if (compileVersion.isPresent() && compileVersion.get().equals(Version.emptyVersion))
            throw new IllegalArgumentException("The empty version is not a legal compile version.");

        this.id = id;
        this.source = source;
        this.authorEmail = authorEmail;
        this.compileVersion = compileVersion;
        this.allowedMajor = requireNonNull(allowedMajor);
        this.buildTime = buildTime;
        this.sourceUrl = requireNonNull(sourceUrl, "sourceUrl cannot be null");
        this.commit = requireNonNull(commit, "commit cannot be null");
        this.bundleHash = bundleHash;
        this.obsoleteAt = obsoleteAt;
        this.hasPackage = hasPackage;
        this.shouldSkip = shouldSkip;
        this.description = description;
        this.submittedAt = requireNonNull(submittedAt);
        this.risk = requireAtLeast(risk, "application build risk", 0);
    }

    public RevisionId id() {
        return id;
    }

    /** Creates a minimal version for a development build. */
    public static ApplicationVersion forDevelopment(RevisionId id, Optional<Version> compileVersion, Optional<Integer> allowedMajor) {
        return new ApplicationVersion(id, Optional.empty(), Optional.empty(), compileVersion, allowedMajor, Optional.empty(),
                                      Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), true, false,
                                      Optional.empty(), Optional.empty(), 0);
    }

    /** Creates a version from a completed build, an author email, and build metadata. */
    public static ApplicationVersion forProduction(RevisionId id, Optional<SourceRevision> source, Optional<String> authorEmail,
                                                   Optional<Version> compileVersion, Optional<Integer> allowedMajor, Optional<Instant> buildTime, Optional<String> sourceUrl,
                                                   Optional<String> commit, Optional<String> bundleHash, Optional<String> description, Instant submittedAt, int risk) {
        return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime,
                                      sourceUrl, commit, bundleHash, Optional.empty(), true, false,
                                      description, Optional.of(submittedAt), risk);
    }

    /** Returns a unique identifier for this version or "unknown" if version is not known */
    // TODO jonmv: kill
    public String stringId() {
        return source.map(SourceRevision::commit).map(ApplicationVersion::abbreviateCommit)
                .or(this::commit)
                .map(commit -> String.format("%s.%d-%s", majorVersion, buildNumber(), commit))
                .orElseGet(() -> majorVersion + "." + buildNumber());
    }

    /**
     * Returns information about the source of this revision, or empty if the source is not know/defined
     * (which is the case for command-line deployment from developers, but never for deployment jobs)
     */
    public Optional<SourceRevision> source() { return source; }

    /** Returns the build number of this version */
    public long buildNumber() { return id.number(); }

    /** Returns the email of the author of commit of this version, if known */
    public Optional<String> authorEmail() { return authorEmail; }

    /** Returns the Vespa version this package was compiled against, if known. */
    public Optional<Version> compileVersion() { return compileVersion; }

    public Optional<Integer> allowedMajor() { return allowedMajor; }

    /** Returns the time this package was built, if known. */
    public Optional<Instant> buildTime() { return buildTime; }

    /** Returns the hash of app package except deployment/build-meta data */
    public Optional<String> bundleHash() {
        return bundleHash;
    }

    /** Returns the source URL for this application version. */
    public Optional<String> sourceUrl() {
        return sourceUrl.or(() -> source.map(source -> {
            String repository = source.repository();
            if (repository.startsWith("git@"))
                repository = "https://" + repository.substring(4).replace(':', '/');
            if (repository.endsWith(".git"))
                repository = repository.substring(0, repository.length() - 4);
            return repository + "/tree/" + source.commit();
        }));
    }

    /** Returns the commit name of this application version. */
    public Optional<String> commit() { return commit.or(() -> source.map(SourceRevision::commit)); }

    /** Returns whether the application package for this version was deployed directly to zone */
    public boolean isDeployedDirectly() {
        return ! id.isProduction();
    }

    /** Returns a copy of this without a package stored. */
    public ApplicationVersion withoutPackage() {
        return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime, sourceUrl, commit, bundleHash, obsoleteAt, false, shouldSkip, description, submittedAt, risk);
    }

    /** Returns a copy of this which is obsolete now. */
    public ApplicationVersion obsoleteAt(Instant now) {
        return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime, sourceUrl, commit, bundleHash, Optional.of(now), hasPackage, shouldSkip, description, submittedAt, risk);
    }

    /** Returns the instant at which this became obsolete, i.e., no longer relevant for automated deployments. */
    public Optional<Instant> obsoleteAt() {
        return obsoleteAt;
    }

    /** Whether we still have the package for this revision. */
    public boolean hasPackage() {
        return hasPackage;
    }

    /** Returns a copy of this which will not be rolled out to production. */
    public ApplicationVersion skipped() {
        return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime, sourceUrl, commit, bundleHash, obsoleteAt, hasPackage, true, description, submittedAt, risk);
    }

    /** Whether we have chosen to skip this version. */
    public boolean shouldSkip() {
        return shouldSkip;
    }

    /** Whether this revision can be deployed. */
    public boolean isDeployable() {
        return hasPackage && ! shouldSkip;
    }

    /** An optional, free-text description on this build. */
    public Optional<String> description() {
        return description;
    }

    /** Instant at which this version was submitted to the build system. */
    public Optional<Instant> submittedAt() {
        return submittedAt;
    }

    /** The assumed risk of rolling out this revision, relative to the previous. */
    public int risk() {
        return risk;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if ( ! (o instanceof ApplicationVersion)) return false;
        ApplicationVersion that = (ApplicationVersion) o;
        return id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }

    @Override
    public String toString() {
        return id +
               source.map(s -> ", " + s).orElse("") +
               authorEmail.map(e -> ", by " + e).orElse("") +
               compileVersion.map(v -> ", built against " + v).orElse("") +
               buildTime.map(t -> " at " + t).orElse("") ;
    }

    /** Abbreviate given commit hash to 9 characters */
    private static String abbreviateCommit(String hash) {
        return hash.length() <= 9 ? hash : hash.substring(0, 9);
    }

    @Override
    public int compareTo(ApplicationVersion o) {
        return id.compareTo(o.id);
    }

}