diff options
Diffstat (limited to 'controller-api/src')
4 files changed, 230 insertions, 65 deletions
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 index 71ad7df6249..74f02172af0 100644 --- 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 @@ -3,36 +3,46 @@ package com.yahoo.vespa.hosted.controller.api.integration.organization; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; +import java.util.Objects; import java.util.Optional; public class Issue { private final String summary; private final String description; + private final String label; private final User assignee; private final PropertyId propertyId; - private Issue(String summary, String description, User assignee, PropertyId propertyId) { + private Issue(String summary, String description, String label, User assignee, PropertyId propertyId) { + if (summary.isEmpty()) throw new IllegalArgumentException("Issue summary can not be empty!"); + if (description.isEmpty()) throw new IllegalArgumentException("Issue description can not be empty!"); + Objects.requireNonNull(propertyId, "An issue must belong to a property!"); + this.summary = summary; this.description = description; + this.label = label; this.assignee = assignee; this.propertyId = propertyId; } - public Issue(String summary, String description) { - this(summary, description, null, null); + public Issue(String summary, String description, PropertyId propertyId) { + this(summary, description, null, null, propertyId); } public Issue append(String appendage) { - return new Issue(summary, description + appendage, assignee, propertyId); + return new Issue(summary, description + appendage, label, assignee, propertyId); } - public Issue withUser(User assignee) { - return new Issue(summary, description, assignee, propertyId); + public Issue withLabel(String label) { + return new Issue(summary, description, label, assignee, propertyId); + } + public Issue withAssignee(User assignee) { + return new Issue(summary, description, label, assignee, propertyId); } public Issue withPropertyId(PropertyId propertyId) { - return new Issue(summary, description, assignee, propertyId); + return new Issue(summary, description, label, assignee, propertyId); } public String summary() { @@ -43,12 +53,16 @@ public class Issue { return description; } + public Optional<String> label() { + return Optional.ofNullable(label); + } + public Optional<User> assignee() { return Optional.ofNullable(assignee); } - public Optional<PropertyId> propertyId() { - return Optional.ofNullable(propertyId); + public PropertyId propertyId() { + return propertyId; } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockOrganization.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockOrganization.java new file mode 100644 index 00000000000..51bba7bb52c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockOrganization.java @@ -0,0 +1,146 @@ +package com.yahoo.vespa.hosted.controller.api.integration.organization; + +import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; + +import java.net.URI; +import java.time.Clock; +import java.time.Duration; +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; + +public class MockOrganization implements Organization { + + private final Clock clock; + private final AtomicLong counter; + private final HashMap<IssueId, WrappedIssue> issues; + private final HashMap<PropertyId, PropertyInfo> properties; + + public MockOrganization(Clock clock) { + this.clock = clock; + + counter = new AtomicLong(); + issues = new HashMap<>(); + properties = new HashMap<>(); + } + + @Override + public IssueId file(Issue issue) { + IssueId issueId = IssueId.from("" + counter.incrementAndGet()); + issues.put(issueId, new WrappedIssue(issue)); + return issueId; + } + + @Override + public Optional<IssueId> findBySimilarity(Issue issue) { + return issues.entrySet().stream() + .filter(entry -> entry.getValue().issue.summary().equals(issue.summary())) + .findFirst() + .map(Map.Entry::getKey); + } + + @Override + public void update(IssueId issueId, String description) { + touch(issueId); + } + + @Override + public void commentOn(IssueId issueId, String comment) { + touch(issueId); + } + + @Override + public boolean isOpen(IssueId issueId) { + return issues.get(issueId).open; + } + + @Override + public boolean isActive(IssueId issueId, Duration maxInactivity) { + return issues.get(issueId).updated.isAfter(clock.instant().minus(maxInactivity)); + } + + @Override + public Optional<User> assigneeOf(IssueId issueId) { + return Optional.ofNullable(issues.get(issueId).assignee); + } + + @Override + public boolean reassign(IssueId issueId, User assignee) { + issues.get(issueId).assignee = assignee; + touch(issueId); + return true; + } + + @Override + public List<? extends List<? extends User>> contactsFor(PropertyId propertyId) { + return properties.get(propertyId).contacts; + } + + @Override + public URI issueCreationUri(PropertyId propertyId) { + return null; + } + + @Override + public URI contactsUri(PropertyId propertyId) { + return null; + } + + @Override + public URI propertyUri(PropertyId propertyId) { + return null; + } + + public void close(IssueId issueId) { + issues.get(issueId).open = false; + touch(issueId); + } + + public void setDefaultAssigneeFor(PropertyId propertyId, User defaultAssignee) { + properties.get(propertyId).defaultAssignee = defaultAssignee; + } + + public void setContactsFor(PropertyId propertyId, List<List<User>> contacts) { + properties.get(propertyId).contacts = contacts; + } + + public void addProperty(PropertyId propertyId) { + properties.put(propertyId, new PropertyInfo()); + } + + private void touch(IssueId issueId) { + issues.get(issueId).updated = clock.instant(); + } + + + private class WrappedIssue { + + private Issue issue; + private Instant updated; + private boolean open; + private User assignee; + + private WrappedIssue(Issue issue) { + this.issue = issue; + + updated = clock.instant(); + open = true; + assignee = issue.assignee().orElse(properties.get(issue.propertyId()).defaultAssignee); + } + + } + + + private class PropertyInfo { + + private User defaultAssignee; + private List<List<User>> contacts = Collections.emptyList(); + + } + +} + 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 index 7e07a710e37..913c2d96a14 100644 --- 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 @@ -2,25 +2,15 @@ package com.yahoo.vespa.hosted.controller.api.integration.organization; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; +import java.io.UncheckedIOException; +import java.net.URI; import java.time.Duration; import java.util.List; -import java.util.stream.Collectors; +import java.util.Optional; 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. @@ -29,47 +19,52 @@ public interface Organization { IssueId file(Issue issue); /** - * File the given issue, or update it if is already exists (based on similarity). + * Returns the ID of this issue, if it exists and is open, based on a similarity search. * - * @param issue The issue to file or update. - * @return ID of the created or updated issue. + * @param issue The issue to search for; relevant fields are the summary and the owner (propertyId). + * @return ID of the issue, if it is found. */ - IssueId fileOrUpdate(Issue issue); + Optional<IssueId> findBySimilarity(Issue issue); /** - * Reassign an issue to the Vespa operations team for termination. + * Update the description of the issue with the given ID. * - * @param issueId ID of the issue to reassign. + * @param issueId ID of the issue to comment on. + * @param description The updated description. */ - void terminate(IssueId issueId); + void update(IssueId issueId, String description); /** - * Escalate an issue filed with the given property. + * Add a comment to the issue with the given ID. * - * @param issueId ID of the issue to escalate. - * @param propertyId PropertyId of the tenant owning the application for which the issue was filed. + * @param issueId ID of the issue to comment on. + * @param comment The comment to add. */ - default void escalate(IssueId issueId, PropertyId propertyId) { - for (User target : escalationTargetsFrom(contactsFor(propertyId), assigneeOf(issueId))) - if (reassign(issueId, target)) - break; - } + void commentOn(IssueId issueId, String comment); /** - * Returns the user assigned to the given issue, if any. + * Returns whether the issue is still under investigation. * - * @param issueId ID of the issue for which to find the assignee. - * @return The user responsible for fixing the given issue, if found. + * @param issueId ID of the issue to examine. + * @return Whether the given issue is under investigation. */ - User assigneeOf(IssueId issueId); + boolean isOpen(IssueId issueId); /** - * Add a comment to the issue with the given ID. + * Returns whether there has been significant activity on the issue within the given duration. * - * @param issueId ID of the issue to comment on. - * @param comment The comment to add. + * @param issueId ID of the issue to examine. + * @return Whether the given issue is actively worked on. */ - void comment(IssueId issueId, String comment); + boolean isActive(IssueId issueId, Duration maxInactivity); + + /** + * 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. + */ + Optional<User> assigneeOf(IssueId issueId); /** * Reassign the issue with the given ID to the given user, and returns the outcome of this. @@ -81,28 +76,42 @@ public interface Organization { 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. + * Escalate an issue filed with the given property. * - * @param issueId ID of the issue to examine. - * @return Whether the given issue is actively worked on. + * @param issueId ID of the issue to escalate. + * @param propertyId PropertyId of the tenant owning the application for which the issue was filed. */ - boolean isActive(IssueId issueId, Duration maxInactivityAge); + default boolean escalate(IssueId issueId, PropertyId propertyId) { + List<? extends List<? extends User>> contacts = contactsFor(propertyId); + + Optional<User> assignee = assigneeOf(issueId); + int assigneeLevel = -1; + if (assignee.isPresent()) + for (int level = contacts.size(); --level > assigneeLevel; ) + if (contacts.get(level).contains(assignee.get())) + assigneeLevel = level; + + for (int level = assigneeLevel + 1; level < contacts.size(); level++) + for (User target : contacts.get(level)) + if (reassign(issueId, target)) + return true; + + return false; + } /** * 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. + * @return A sorted, nested, reverse sorted list of contacts. */ - List<List<User>> contactsFor(PropertyId propertyId); + List<? extends List<? extends User>> contactsFor(PropertyId propertyId); + + URI issueCreationUri(PropertyId propertyId); + + URI contactsUri(PropertyId propertyId); + + URI propertyUri(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 index 60a94e04116..13141ee2adb 100644 --- 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 @@ -5,10 +5,6 @@ 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) { @@ -21,7 +17,7 @@ public class User { public static User from(String username) { if (username.isEmpty()) - throw new IllegalArgumentException("username may not be empty"); + throw new IllegalArgumentException("Username may not be empty!"); return new User(username); } @@ -29,7 +25,7 @@ public class User { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if ( ! (o instanceof User)) return false; User that = (User) o; return Objects.equals(username, that.username); } |