summaryrefslogtreecommitdiffstats
path: root/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java
blob: 1f387a49a68a424842416e90673c73f036c8a7a7 (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
// Copyright Yahoo. 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.Objects;
import java.util.Optional;
import java.util.OptionalLong;

/**
 * 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> {

    /**
     * Used in cases where application version cannot be determined, such as manual deployments (e.g. in dev
     * environment)
     */
    public static final ApplicationVersion unknown = new ApplicationVersion(Optional.empty(), OptionalLong.empty(),
                                                                            Optional.empty(), Optional.empty(), Optional.empty(),
                                                                            Optional.empty(), Optional.empty(), true);

    // 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 Optional<SourceRevision> source;
    private final Optional<String> authorEmail;
    private final OptionalLong buildNumber;
    private final Optional<Version> compileVersion;
    private final Optional<Instant> buildTime;
    private final Optional<String> sourceUrl;
    private final Optional<String> commit;
    private final boolean deployedDirectly;

    /** Public for serialisation only. */
    public ApplicationVersion(Optional<SourceRevision> source, OptionalLong buildNumber, Optional<String> authorEmail,
                              Optional<Version> compileVersion, Optional<Instant> buildTime, Optional<String> sourceUrl,
                              Optional<String> commit, boolean deployedDirectly) {
        if (buildNumber.isEmpty() && (   source.isPresent() || authorEmail.isPresent() || compileVersion.isPresent()
                                      || buildTime.isPresent() || sourceUrl.isPresent() || commit.isPresent()))
            throw new IllegalArgumentException("Build number must be present if any other attribute is");

        if (buildNumber.isPresent() && buildNumber.getAsLong() <= 0)
            throw new IllegalArgumentException("Build number must be > 0");

        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.source = source;
        this.buildNumber = buildNumber;
        this.authorEmail = authorEmail;
        this.compileVersion = compileVersion;
        this.buildTime = buildTime;
        this.sourceUrl = Objects.requireNonNull(sourceUrl, "sourceUrl cannot be null");
        this.commit = Objects.requireNonNull(commit, "commit cannot be null");
        this.deployedDirectly = deployedDirectly;
    }

    /** Create an application package version from a completed build, without an author email */
    public static ApplicationVersion from(SourceRevision source, long buildNumber) {
        return new ApplicationVersion(Optional.of(source), OptionalLong.of(buildNumber), Optional.empty(),
                                      Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), false);
    }

    /** Creates an version from a completed build, an author email, and build meta data. */
    public static ApplicationVersion from(SourceRevision source, long buildNumber, String authorEmail,
                                          Version compileVersion, Instant buildTime) {
        return new ApplicationVersion(Optional.of(source), OptionalLong.of(buildNumber), Optional.of(authorEmail),
                                      Optional.of(compileVersion), Optional.of(buildTime), Optional.empty(), Optional.empty(), false);
    }

    /** Creates an version from a completed build, an author email, and build meta data. */
    public static ApplicationVersion from(Optional<SourceRevision> source, long buildNumber, Optional<String> authorEmail,
                                          Optional<Version> compileVersion, Optional<Instant> buildTime,
                                          Optional<String> sourceUrl, Optional<String> commit, boolean deployedDirectly) {
        return new ApplicationVersion(source, OptionalLong.of(buildNumber), authorEmail, compileVersion, buildTime, sourceUrl, commit, deployedDirectly);
    }

    /** Returns an unique identifier for this version or "unknown" if version is not known */
    public String id() {
        if (isUnknown()) return "unknown";

        return source.map(SourceRevision::commit).map(ApplicationVersion::abbreviateCommit)
                .or(this::commit)
                .map(commit -> String.format("%s.%d-%s", majorVersion, buildNumber.getAsLong(), commit))
                .orElseGet(() -> majorVersion + "." + buildNumber.getAsLong());
    }

    /**
     * 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 that built this version */
    public OptionalLong buildNumber() { return buildNumber; }

    /** 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; }

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

    /** 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 this is unknown */
    public boolean isUnknown() {
        return this.equals(unknown);
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if ( ! (o instanceof ApplicationVersion)) return false;
        ApplicationVersion that = (ApplicationVersion) o;
        return    Objects.equals(buildNumber, that.buildNumber)
               && Objects.equals(commit(), that.commit())
               && deployedDirectly == that.deployedDirectly;
    }

    @Override
    public int hashCode() {
        return Objects.hash(buildNumber, commit(), deployedDirectly);
    }

    @Override
    public String toString() {
        return   "Application package version: " + id()
               + source.map(s -> ", " + s.toString()).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) {
        if (buildNumber().isEmpty() || o.buildNumber().isEmpty())
            return Boolean.compare(buildNumber().isPresent(), o.buildNumber.isPresent()); // Unknown version sorts first

        if (deployedDirectly || o.deployedDirectly)
            return Boolean.compare(deployedDirectly, o.deployedDirectly); // Directly deployed versions sort first

        return Long.compare(buildNumber().getAsLong(), o.buildNumber().getAsLong());
    }

}