summaryrefslogtreecommitdiffstats
path: root/controller-api
diff options
context:
space:
mode:
authorJon Marius Venstad <jvenstad@yahoo-inc.com>2017-10-20 09:58:59 +0200
committerJon Marius Venstad <jvenstad@yahoo-inc.com>2017-10-20 09:58:59 +0200
commite5e197ec9390033da499cebfb68ba92ac74cb17b (patch)
treebce37c26ab829ec8db428f72b5aef822cedc16b3 /controller-api
parentb41d2e64fddaaba2763db313423652dfd4d0912c (diff)
Refactored deployment issues >_<
Diffstat (limited to 'controller-api')
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/AutonomousCarbonUnitsWhoFixApplications.java67
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Contacts.java136
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Issues.java186
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Properties.java16
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraMock.java50
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java26
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java54
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java46
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java108
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java47
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/ContactsMock.java31
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java96
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingIssues.java95
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/PropertiesMock.java26
14 files changed, 377 insertions, 607 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/AutonomousCarbonUnitsWhoFixApplications.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/AutonomousCarbonUnitsWhoFixApplications.java
deleted file mode 100644
index eff62b594c0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/AutonomousCarbonUnitsWhoFixApplications.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.config.provision.ApplicationId;
-
-import java.util.Collection;
-
-/**
- * Represents the people responsible for keeping Vespa up and running in a given organization, etc..
- *
- * @author jvenstad
- */
-public interface AutonomousCarbonUnitsWhoFixApplications {
-
- /**
- * Notifies those responsible for the application with the given ID of failing deployments.
- *
- * @param applicationId ID of the application with failing deployments.
- * @return ID of the created issue.
- */
- IssueId fileIssue(ApplicationId applicationId);
-
- /**
- * Notifies those responsible for the Vespa platform that too many applications are failing.
- *
- * @param applicationIds IDs of all applications with failing deployments.
- * @return ID of the created issue.
- */
- IssueId fileIssue(Collection<ApplicationId> applicationIds);
-
- /**
- * @param issueId ID of the issue to escalate.
- */
- void escalateIssue(IssueId issueId);
-
- /**
- * @param issueId ID of the issue to examine.
- * @return Whether the given issue is under investigation.
- */
- boolean isOpen(IssueId issueId);
-
- /**
- * @param issueId IF of the issue to examine.
- * @return Whether the given issue is actively worked on.
- */
- boolean isActive(IssueId issueId);
-
-
- class IssueId {
-
- protected final String id;
-
- protected IssueId(String id) {
- this.id = id;
- }
-
- @Override
- public String toString() {
- return id;
- }
-
- public static IssueId from(String value) {
- return new IssueId(value);
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Contacts.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Contacts.java
deleted file mode 100644
index 329483a85c5..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Contacts.java
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.net.URI;
-import java.util.Collection;
-import java.util.Objects;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.Contacts.Category.unknown;
-import static java.util.Comparator.reverseOrder;
-
-/**
- * @author jvenstad
- */
-public interface Contacts {
-
- /**
- * Returns the most relevant user of lowest non-empty level above that of @assignee, or, if no such user exists,
- * the @assignee with @Category information.
- */
- static UserContact escalationTargetFrom(Collection<UserContact> userContacts, String assignee) {
- return userContacts.stream()
- .filter(contact -> ! contact.username().isEmpty()) // don't assign to empty names
- .sorted(reverseOrder()).distinct() // Pick out the highest category per user.
- // Keep the assignee, or the last user on the first non-empty level above her.
- .sorted().reduce(new UserContact(assignee, assignee, unknown), (current, next) ->
- next.is(assignee) || (current.is(assignee) ^ current.level() == next.level()) ? next : current);
- }
-
- /**
- * Return a list of all contact entries for property with id @propertyId, where username is set.
- */
- Collection<UserContact> userContactsFor(long propertyId);
-
- /** Returns the URL listing contacts for the given property */
- URI contactsUri(long propertyId);
-
- /**
- * Return a target of escalation above @assignee, from the set of @UserContact entries found for @propertyId.
- */
- default UserContact escalationTargetFor(long propertyId, String assignee) {
- return escalationTargetFrom(userContactsFor(propertyId), assignee);
- }
-
- /**
- * A list of contact roles, in the order in which we look for escalation targets.
- * Categories must be listed in increasing order of relevancy per level, and by increasing level.
- */
- enum Category {
-
- unknown(-1, Level.none, "Unknown"),
- admin(54, Level.grunt, "Administrator"), // TODO: Find more grunts?
- businessOwner(567, Level.owner, "Business Owner"),
- serviceOwner(646, Level.owner, "Service Engineering Owner"),
- engineeringOwner(566, Level.owner, "Engineering Owner"),
- vpBusiness(11, Level.VP, "VP Business"),
- vpService(647, Level.VP, "VP Service Engineering"),
- vpEngineering(9, Level.VP, "VP Engineering");
-
- public final long id;
- public final Level level;
- public final String name;
-
- Category(long id, Level level, String name) {
- this.id = id;
- this.level = level;
- this.name = name;
- }
-
- /** Find the category for the given id, or unknown if the id is unknown. */
- public static Category of(Long id) {
- for (Category category : values())
- if (category.id == id)
- return category;
- return unknown;
- }
-
- public enum Level {
- none,
- grunt,
- owner,
- VP;
- }
-
- }
-
- /** Container class for user contact information; sorts by category and identifies by username. Immutable. */
- class UserContact implements Comparable<UserContact> {
-
- private final String username;
- private final String name;
- private final Category category;
-
- public UserContact(String username, String name, Category category) {
- Objects.requireNonNull(username, "username cannot be null");
- Objects.requireNonNull(name, "name cannot be null");
- Objects.requireNonNull(category, "category cannot be null");
- this.username = username;
- this.name = name;
- this.category = category;
- }
-
- public String username() { return username; }
- public String name() { return name; }
- public Category category() { return category; }
- public Category.Level level() { return category.level; }
-
- public boolean is(String username) { return this.username.equals(username); }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- UserContact that = (UserContact) o;
- return Objects.equals(username, that.username);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(username);
- }
-
- @Override
- public int compareTo(@NotNull UserContact other) {
- return category().compareTo(other.category());
- }
-
- @Override
- public String toString() {
- return String.format("%s, %s, %s", username, name, category.name);
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Issues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Issues.java
deleted file mode 100644
index 8f24b4e3ede..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Issues.java
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * @author jvenstad
- */
-public interface Issues {
-
- /**
- * Returns information about an issue.
- * If this issue does not exist this returns an issue id containing the id and default values.
- */
- IssueInfo fetch(String issueId);
-
- /**
- * Returns the @Meta of all unresolved issues which have the same summary (and queue, if present) as @issue.
- */
- List<IssueInfo> fetchSimilarTo(Issue issue);
-
- /**
- * Files the given issue
- *
- * @return the id of the created issue
- */
- String file(Issue issue);
-
- /**
- * Update the description fields of the issue stored with id @issueId to be @description.
- */
- void update(String issueId, String description);
-
- /**
- * Set the assignee of the issue with id @issueId to the user with usename @assignee.
- */
- void reassign(String issueId, String assignee);
-
- /**
- * Add the user with username @watcher to the watcher list of the issue with id @issueId.
- */
- void addWatcher(String issueId, String watcher);
-
- /**
- * Post @comment as a comment to the issue with id @issueId.
- */
- void comment(String issueId, String comment);
-
-
- /** Contains information used to file an issue with the responsible party; only @queue is mandatory. */
- class Classification {
-
- private final String queue;
- private final String component;
- private final String label;
- private final String assignee;
-
- public Classification(String queue, String component, String label, String assignee) {
- if (queue.isEmpty()) throw new IllegalArgumentException("Queue can not be empty!");
-
- this.queue = queue;
- this.component = component;
- this.label = label;
- this.assignee = assignee;
- }
-
- public Classification(String queue) {
- this(queue, null, null, null);
- }
-
- public Classification withComponent(String component) { return new Classification(queue, component, label, assignee); }
- public Classification withLabel(String label) { return new Classification(queue, component, label, assignee); }
- public Classification withAssignee(String assignee) { return new Classification(queue, component, label, assignee); }
-
- public String queue() { return queue; }
- public Optional<String> component() { return Optional.ofNullable(component); }
- public Optional<String> label() { return Optional.ofNullable(label); }
- public Optional<String> assignee() { return Optional.ofNullable(assignee); }
-
- @Override
- public String toString() {
- return "Queue : " + queue() + "\n" +
- "Component : " + component() + "\n" +
- "Label : " + label() + "\n" +
- "Assignee : " + assignee() + "\n";
- }
-
- }
-
-
- /** Information about a stored issue */
- class IssueInfo {
-
- private final String id;
- private final String key;
- private final Instant updated;
- private final Optional<String> assignee;
- private final Status status;
-
- public IssueInfo(String id, String key, Instant updated, Optional<String> assignee, Status status) {
- if (assignee == null || assignee.isPresent() && assignee.get().isEmpty()) // TODO: Throw on these things
- assignee = Optional.empty();
- this.id = id;
- this.key = key;
- this.updated = updated;
- this.assignee = assignee;
- this.status = status;
- }
-
- public IssueInfo withAssignee(Optional<String> assignee) {
- return new IssueInfo(id, key, updated, assignee, status);
- }
-
- public String id() { return id; }
- public String key() { return key; }
- public Instant updated() { return updated; }
- public Optional<String> assignee() { return assignee; }
- public Status status() { return status; }
-
- public enum Status {
-
- toDo("To Do"),
- inProgress("In Progress"),
- done("Done"),
- noCategory("No Category");
-
- private final String value;
-
- Status(String value) { this.value = value; }
-
- public static Status fromValue(String value) {
- for (Status status : Status.values())
- if (status.value.equals(value))
- return status;
- throw new IllegalArgumentException(value + " is not a valid status.");
- }
-
- }
-
- }
-
-
- /**
- * A representation of an issue with a Vespa application which can be reported and escalated through an external issue service.
- * This class is immutable.
- *
- * @author jvenstad
- */
- class Issue {
-
- private final String summary;
- private final String description;
- private final Classification classification;
-
- public Issue(String summary, String description, Classification classification) {
- if (summary.isEmpty()) throw new IllegalArgumentException("Summary can not be empty.");
- if (description.isEmpty()) throw new IllegalArgumentException("Description can not be empty.");
-
- this.summary = summary;
- this.description = description;
- this.classification = classification;
- }
-
- public Issue(String summary, String description) {
- this(summary, description, null);
- }
-
- public Issue with(Classification classification) {
- return new Issue(summary, description, classification);
- }
- public Issue withDescription(String description) { return new Issue(summary, description, classification); }
-
- /** Return a new @Issue with the description of @this, but with @appendage appended. */
- public Issue append(String appendage) {
- return new Issue(summary, description + "\n\n" + appendage, classification);
- }
-
- public String summary() { return summary; }
- public String description() { return description; }
- public Optional<Classification> classification() { return Optional.ofNullable(classification); }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Properties.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Properties.java
deleted file mode 100644
index 652b5495bc5..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/Properties.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import java.util.Optional;
-
-/**
- * @author jvenstad
- */
-public interface Properties {
-
- /**
- * Return the @Issues.Classification listed for the property with id @propertyId.
- */
- Optional<Issues.Classification> classificationFor(long propertyId);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraMock.java
deleted file mode 100644
index da653ddd8a8..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraMock.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.jira;
-
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * @author jvenstad
- */
-// TODO: Make mock.
-public class JiraMock implements Jira {
-
- public final Map<String, JiraCreateIssue.JiraFields> issues = new HashMap<>();
-
- private Long counter = 0L;
-
- @Override
- public List<JiraIssue> searchByProjectAndSummary(String project, String summary) {
- return issues.entrySet().stream()
- .filter(entry -> entry.getValue().project.key.equals(project))
- .filter(entry -> entry.getValue().summary.contains(summary))
- .map(entry -> new JiraIssue(entry.getKey(), new JiraIssue.Fields(Instant.now())))
- .collect(Collectors.toList());
- }
-
- @Override
- public JiraIssue createIssue(JiraCreateIssue issueData) {
- JiraIssue issue = uniqueKey();
- issues.put(issue.key, issueData.fields);
- return issue;
- }
-
- @Override
- public void commentIssue(JiraIssue issue, JiraComment comment) {
- // Add mock when relevant.
- }
-
- @Override
- public void addAttachment(JiraIssue issue, String filename, String fileContent) {
- // Add mock when relevant.
- }
-
- private JiraIssue uniqueKey() {
- return new JiraIssue((++counter).toString(), new JiraIssue.Fields(Instant.now()));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java
new file mode 100644
index 00000000000..7874fcd8c45
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java
@@ -0,0 +1,26 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.organization;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Optional;
+
+/**
+ * Represents the people responsible for keeping Vespa up and running in a given organization, etc..
+ *
+ * @author jvenstad
+ */
+public interface DeploymentIssues {
+
+ IssueId fileUnlessOpen(Optional<IssueId> issueId, ApplicationId applicationId, PropertyId propertyId);
+
+ IssueId fileUnlessOpen(Optional<IssueId> issueId, ApplicationId applicationId, User assignee);
+
+ IssueId fileUnlessOpen(Collection<ApplicationId> applicationIds);
+
+ void escalateIfInactive(IssueId issueId, Optional<PropertyId> propertyId, Duration maxInactivity);
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java
new file mode 100644
index 00000000000..71ad7df6249
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java
@@ -0,0 +1,54 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.organization;
+
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+
+import java.util.Optional;
+
+public class Issue {
+
+ private final String summary;
+ private final String description;
+ private final User assignee;
+ private final PropertyId propertyId;
+
+ private Issue(String summary, String description, User assignee, PropertyId propertyId) {
+ this.summary = summary;
+ this.description = description;
+ this.assignee = assignee;
+ this.propertyId = propertyId;
+ }
+
+ public Issue(String summary, String description) {
+ this(summary, description, null, null);
+ }
+
+ public Issue append(String appendage) {
+ return new Issue(summary, description + appendage, assignee, propertyId);
+ }
+
+ public Issue withUser(User assignee) {
+ return new Issue(summary, description, assignee, propertyId);
+ }
+
+ public Issue withPropertyId(PropertyId propertyId) {
+ return new Issue(summary, description, assignee, propertyId);
+ }
+
+ public String summary() {
+ return summary;
+ }
+
+ public String description() {
+ return description;
+ }
+
+ public Optional<User> assignee() {
+ return Optional.ofNullable(assignee);
+ }
+
+ public Optional<PropertyId> propertyId() {
+ return Optional.ofNullable(propertyId);
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java
new file mode 100644
index 00000000000..42524c865c9
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java
@@ -0,0 +1,46 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.organization;
+
+import java.util.Objects;
+
+/**
+ * @author jvenstad
+ */
+public class IssueId {
+
+ protected final String id;
+
+ protected IssueId(String id) {
+ this.id = id;
+ }
+
+ public static IssueId from(String value) {
+ if (value.isEmpty())
+ throw new IllegalArgumentException("Can not make an IssueId from an empty value.");
+
+ return new IssueId(value);
+ }
+
+ public String value() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return value();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IssueId issueId = (IssueId) o;
+ return Objects.equals(id, issueId.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java
new file mode 100644
index 00000000000..7e07a710e37
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java
@@ -0,0 +1,108 @@
+package com.yahoo.vespa.hosted.controller.api.integration.organization;
+
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public interface Organization {
+
+ /**
+ * Returns a flat list of all escalation targets among the given users.
+ * An escalation target is anyone of higher rank than the given assignee.
+ */
+ static List<User> escalationTargetsFrom(List<List<User>> contacts, User assignee) {
+ for (int i = 0; i < contacts.size(); i++)
+ if (contacts.get(i).contains(assignee))
+ return contacts.subList(i + 1, contacts.size()).stream().flatMap(List::stream).collect(Collectors.toList());
+
+ return contacts.stream().flatMap(List::stream).collect(Collectors.toList());
+ }
+
+ /**
+ * File an issue with its given property or the default, and with the specific assignee, if present.
+ *
+ * @param issue The issue to file.
+ * @return ID of the created issue.
+ */
+ IssueId file(Issue issue);
+
+ /**
+ * File the given issue, or update it if is already exists (based on similarity).
+ *
+ * @param issue The issue to file or update.
+ * @return ID of the created or updated issue.
+ */
+ IssueId fileOrUpdate(Issue issue);
+
+ /**
+ * Reassign an issue to the Vespa operations team for termination.
+ *
+ * @param issueId ID of the issue to reassign.
+ */
+ void terminate(IssueId issueId);
+
+ /**
+ * Escalate an issue filed with the given property.
+ *
+ * @param issueId ID of the issue to escalate.
+ * @param propertyId PropertyId of the tenant owning the application for which the issue was filed.
+ */
+ default void escalate(IssueId issueId, PropertyId propertyId) {
+ for (User target : escalationTargetsFrom(contactsFor(propertyId), assigneeOf(issueId)))
+ if (reassign(issueId, target))
+ break;
+ }
+
+ /**
+ * Returns the user assigned to the given issue, if any.
+ *
+ * @param issueId ID of the issue for which to find the assignee.
+ * @return The user responsible for fixing the given issue, if found.
+ */
+ User assigneeOf(IssueId issueId);
+
+ /**
+ * Add a comment to the issue with the given ID.
+ *
+ * @param issueId ID of the issue to comment on.
+ * @param comment The comment to add.
+ */
+ void comment(IssueId issueId, String comment);
+
+ /**
+ * Reassign the issue with the given ID to the given user, and returns the outcome of this.
+ *
+ * @param issueId ID of the issue to be reassigned.
+ * @param assignee User to which the issue shall be assigned.
+ * @return Whether the reassignment was successful.
+ */
+ boolean reassign(IssueId issueId, User assignee);
+
+ /**
+ * Returns whether the issue is still under investigation.
+ *
+ * @param issueId ID of the issue to examine.
+ * @return Whether the given issue is under investigation.
+ */
+ boolean isOpen(IssueId issueId);
+
+ /**
+ * Returns whether there has been significant activity on the issue within the given duration.
+ *
+ * @param issueId ID of the issue to examine.
+ * @return Whether the given issue is actively worked on.
+ */
+ boolean isActive(IssueId issueId, Duration maxInactivityAge);
+
+ /**
+ * Returns a nested list where the entries have increasing rank, and where each entry is
+ * a list of the users of that rank, by decreasing relevance.
+ *
+ * @param propertyId ID of the property for which to list contacts.
+ * @return A sorted, nested, sorted list of contacts.
+ */
+ List<List<User>> contactsFor(PropertyId propertyId);
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java
new file mode 100644
index 00000000000..60a94e04116
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java
@@ -0,0 +1,47 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.organization;
+
+import java.util.Objects;
+
+public class User {
+
+ public static final User none = new User("") {
+ public String toString() { return "no one"; }
+ };
+
+ private final String username;
+
+ protected User(String username) {
+ this.username = username;
+ }
+
+ public String username() {
+ return username;
+ }
+
+ public static User from(String username) {
+ if (username.isEmpty())
+ throw new IllegalArgumentException("username may not be empty");
+
+ return new User(username);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ User that = (User) o;
+ return Objects.equals(username, that.username);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(username);
+ }
+
+ @Override
+ public String toString() {
+ return username();
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/ContactsMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/ContactsMock.java
deleted file mode 100644
index 9114cf20ccc..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/ContactsMock.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.vespa.hosted.controller.api.integration.Contacts;
-
-import java.net.URI;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * @author mpolden
- */
-public class ContactsMock implements Contacts {
-
- private final Map<Long, List<UserContact>> userContacts = new HashMap<>();
-
- public void addContact(long propertyId, List<UserContact> contacts) {
- userContacts.put(propertyId, contacts);
- }
-
- public List<UserContact> userContactsFor(long propertyId) {
- return userContacts.get(propertyId);
- }
-
- @Override
- public URI contactsUri(long propertyId) {
- return URI.create("http://contacts.test?propertyId=" + propertyId);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java
new file mode 100644
index 00000000000..62dde3efe55
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java
@@ -0,0 +1,96 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.controller.api.integration.stubs;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
+import org.jetbrains.annotations.TestOnly;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+/**
+ * A memory backed implementation of the Issues API which logs changes and does nothing else.
+ *
+ * @author bratseth, jvenstad
+ */
+public class LoggingDeploymentIssues implements DeploymentIssues {
+
+ private static final Logger log = Logger.getLogger(LoggingDeploymentIssues.class.getName());
+
+ /** Whether the platform is currently broken. */
+ protected final AtomicBoolean platformIssue = new AtomicBoolean(false);
+ /** Last updates for each issue -- used to determine if issues are already logged and when to escalate. */
+ protected final Map<IssueId, Instant> issueUpdates = new HashMap<>();
+
+ /** Used to fabricate unique issue ids. */
+ private final AtomicLong issueIdSequence = new AtomicLong(0);
+
+ private final Clock clock;
+
+ @SuppressWarnings("unused") // Created by dependency injection.
+ @Inject
+ public LoggingDeploymentIssues() {
+ this(Clock.systemUTC());
+ }
+
+ @TestOnly
+ protected LoggingDeploymentIssues(Clock clock) {
+ this.clock = clock;
+ }
+
+ @Override
+ public IssueId fileUnlessOpen(Optional<IssueId> issueId, ApplicationId applicationId, PropertyId propertyId) {
+ return fileUnlessPresent(issueId, applicationId);
+ }
+
+ @Override
+ public IssueId fileUnlessOpen(Optional<IssueId> issueId, ApplicationId applicationId, User assignee) {
+ return fileUnlessPresent(issueId, applicationId);
+ }
+
+ @Override
+ public IssueId fileUnlessOpen(Collection<ApplicationId> applicationIds) {
+ if ( ! platformIssue.get())
+ log.info("These applications are all failing deployment:\n" + applicationIds);
+
+ platformIssue.set(true);
+ return null;
+ }
+
+ @Override
+ public void escalateIfInactive(IssueId issueId, Optional<PropertyId> propertyId, Duration maxInactivity) {
+ if (issueUpdates.containsKey(issueId) && issueUpdates.get(issueId).isBefore(clock.instant().minus(maxInactivity)))
+ escalateIssue(issueId);
+ }
+
+ protected void escalateIssue(IssueId issueId) {
+ issueUpdates.put(issueId, clock.instant());
+ log.info("Deployment issue " + issueId + " should be escalated.");
+ }
+
+ protected IssueId fileIssue(ApplicationId applicationId) {
+ IssueId issueId = IssueId.from("" + issueIdSequence.incrementAndGet());
+ issueUpdates.put(issueId, clock.instant());
+ log.info("Deployment issue " + issueId +": " + applicationId + " has failing deployments.");
+ return issueId;
+ }
+
+ private IssueId fileUnlessPresent(Optional<IssueId> issueId, ApplicationId applicationId) {
+ platformIssue.set(false);
+ return issueId.filter(issueUpdates::containsKey).orElseGet(() -> fileIssue(applicationId));
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingIssues.java
deleted file mode 100644
index 4801f551307..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingIssues.java
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.vespa.hosted.controller.api.integration.Issues;
-
-import java.time.Instant;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.logging.Logger;
-
-/**
- * An memory backed implementation of the Issues API which logs changes and does nothing else.
- *
- * @author bratseth
- */
-@SuppressWarnings("unused") // created by dependency injection
-public class LoggingIssues implements Issues {
-
- private static final Logger log = Logger.getLogger(LoggingIssues.class.getName());
-
- /** Used to fabricate unique issue ids */
- private AtomicLong issueIdSequence = new AtomicLong(0);
-
- // These two maps should have precisely the same keys
- private final Map<String, Issue> issues = new HashMap<>();
- private final Map<String, IssueInfo> issueInfos = new HashMap<>();
-
- @Override
- public IssueInfo fetch(String issueId) {
- return issueInfos.getOrDefault(issueId,
- new IssueInfo(issueId, null, Instant.ofEpochMilli(0), null, IssueInfo.Status.noCategory));
- }
-
- @Override
- public List<IssueInfo> fetchSimilarTo(Issue issue) {
- return Collections.emptyList();
- }
-
- @Override
- public String file(Issue issue) {
- log.info("Want to file " + issue);
- String issueId = "issue-" + issueIdSequence.getAndIncrement();
- file(issueId, issue);
- return issueId;
- }
-
- private IssueInfo file(String issueId, Issue issue) {
- IssueInfo issueInfo = new IssueInfo(issueId, null, Instant.now(), null, IssueInfo.Status.noCategory);
- issues.put(issueId, issue);
- issueInfos.put(issueId, issueInfo);
- return issueInfo;
- }
-
- @Override
- public void update(String issueId, String description) {
- log.info("Want to update " + issueId);
- issues.put(issueId, requireIssue(issueId).withDescription(description));
- }
-
- @Override
- public void reassign(String issueId, String assignee) {
- log.info("Want to reassign issue " + issueId + " to " + assignee);
- issueInfos.put(issueId, requireInfo(issueId).withAssignee(Optional.of(assignee)));
- }
-
- @Override
- public void addWatcher(String issueId, String watcher) {
- log.info("Want to add watcher " + watcher + " to issue " + issueId);
- }
-
- @Override
- public void comment(String issueId, String comment) {
- log.info("Want to comment on issue " + issueId);
- }
-
- private Issue requireIssue(String issueId) {
- Issue issue = issues.get(issueId);
- if (issue == null)
- throw new IllegalArgumentException("No issue with id '" + issueId + "'");
- return issue;
- }
-
- private IssueInfo requireInfo(String issueId) {
- IssueInfo info = issueInfos.get(issueId);
- if (info != null) // we still remember this issue
- return info;
- else // we forgot this issue (due to restart) - recreate it here to avoid log noise
- return file(issueId, new Issue("(Forgotten)", "(Forgotten)"));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/PropertiesMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/PropertiesMock.java
deleted file mode 100644
index 53a31933e03..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/PropertiesMock.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.vespa.hosted.controller.api.integration.Issues;
-import com.yahoo.vespa.hosted.controller.api.integration.Properties;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * @author mpolden
- */
-public class PropertiesMock implements Properties {
-
- private final Map<Long, Issues.Classification> projects = new HashMap<>();
-
- public void addClassification(long propertyId, String classification) {
- projects.put(propertyId, new Issues.Classification(classification));
- }
-
- public Optional<Issues.Classification> classificationFor(long propertyId) {
- return Optional.ofNullable(projects.get(propertyId));
- }
-
-}