diff options
Diffstat (limited to 'controller-api/src')
14 files changed, 542 insertions, 540 deletions
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..74f02172af0 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java @@ -0,0 +1,68 @@ +// 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.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, 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, PropertyId propertyId) { + this(summary, description, null, null, propertyId); + } + + public Issue append(String appendage) { + return new Issue(summary, description + appendage, label, 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, label, assignee, propertyId); + } + + public String summary() { + return summary; + } + + public String description() { + return description; + } + + public Optional<String> label() { + return Optional.ofNullable(label); + } + + public Optional<User> assignee() { + return Optional.ofNullable(assignee); + } + + public PropertyId propertyId() { + return 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/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 new file mode 100644 index 00000000000..913c2d96a14 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java @@ -0,0 +1,117 @@ +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.Optional; + +public interface Organization { + + /** + * 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); + + /** + * Returns the ID of this issue, if it exists and is open, based on a similarity search. + * + * @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. + */ + Optional<IssueId> findBySimilarity(Issue issue); + + /** + * Update the description of the issue with the given ID. + * + * @param issueId ID of the issue to comment on. + * @param description The updated description. + */ + void update(IssueId issueId, String description); + + /** + * 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 commentOn(IssueId issueId, String comment); + + /** + * 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 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. + * + * @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); + + /** + * 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 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, reverse sorted list of contacts. + */ + 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 new file mode 100644 index 00000000000..13141ee2adb --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java @@ -0,0 +1,43 @@ +// 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 { + + 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 instanceof User)) 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)); - } - -} |