summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Marius Venstad <jonmv@users.noreply.github.com>2022-02-19 15:04:41 +0100
committerGitHub <noreply@github.com>2022-02-19 15:04:41 +0100
commit66c1ab2b3c724580062f13de905861daa02d9460 (patch)
tree4863fea61d9ef743a0c287557d9236154672a3a7
parentb9e1b6dc755d56cef4b8164abf783ec9cfe1ee63 (diff)
parentfb4e9378d2ed85236b958962fdf88b9ff73fc8ff (diff)
Merge pull request #21279 from vespa-engine/freva/notifications
Allow operators to list all notifications
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json18
6 files changed, 61 insertions, 14 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
index 3a5bb96d985..b34d113d331 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
@@ -20,7 +20,8 @@ import java.util.Set;
enum PathGroup {
/** Paths exclusive to operators (including read), used for system management. */
- classifiedOperator("/configserver/v1/{*}",
+ classifiedOperator("/application/v4/notifications",
+ "/configserver/v1/{*}",
"/deployment/v1/{*}"),
/** Paths used for system management by operators. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
index 4fdfdfc461b..c0bd1ac03ff 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.notification;
import com.yahoo.collections.Pair;
import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.text.Text;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -41,6 +42,10 @@ public class NotificationsDb {
this.curatorDb = curatorDb;
}
+ public List<TenantName> listTenantsWithNotifications() {
+ return curatorDb.listTenantsWithNotifications();
+ }
+
public List<Notification> listNotifications(NotificationSource source, boolean productionOnly) {
return curatorDb.readNotifications(source.tenant()).stream()
.filter(notification -> source.contains(notification.source()) && (!productionOnly || notification.source().isProduction()))
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
index 9b1a03039fb..6eaad63251c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
@@ -625,7 +625,7 @@ public class CuratorDb {
}
- public List<TenantName> listNotifications() {
+ public List<TenantName> listTenantsWithNotifications() {
return curator.getChildren(notificationsRoot).stream()
.map(TenantName::from)
.collect(Collectors.toUnmodifiableList());
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 3b4154759ca..faa60afa176 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -144,6 +144,9 @@ import java.util.Optional;
import java.util.OptionalLong;
import java.util.Scanner;
import java.util.StringJoiner;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -233,11 +236,12 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private HttpResponse handleGET(Path path, HttpRequest request) {
if (path.matches("/application/v4/")) return root(request);
+ if (path.matches("/application/v4/notifications")) return notifications(request, Optional.ofNullable(request.getProperty("tenant")), true);
if (path.matches("/application/v4/tenant")) return tenants(request);
if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/access/request/ssh")) return accessRequests(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/info")) return tenantInfo(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/notifications")) return notifications(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/notifications")) return notifications(request, Optional.of(path.get("tenant")), false);
if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request);
if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), Optional.empty(), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request);
@@ -542,26 +546,41 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address()));
}
- private HttpResponse notifications(String tenantName, HttpRequest request) {
- NotificationSource notificationSource = new NotificationSource(TenantName.from(tenantName),
- Optional.ofNullable(request.getProperty("application")).map(ApplicationName::from),
- Optional.ofNullable(request.getProperty("instance")).map(InstanceName::from),
- Optional.empty(), Optional.empty(), Optional.empty(), OptionalLong.empty());
-
+ private HttpResponse notifications(HttpRequest request, Optional<String> tenant, boolean includeTenantFieldInResponse) {
+ boolean productionOnly = showOnlyProductionInstances(request);
+ boolean excludeMessages = "true".equals(request.getProperty("excludeMessages"));
Slime slime = new Slime();
Cursor notificationsArray = slime.setObject().setArray("notifications");
- controller.notificationsDb().listNotifications(notificationSource, showOnlyProductionInstances(request))
- .forEach(notification -> toSlime(notificationsArray.addObject(), notification));
+
+ tenant.map(t -> Stream.of(TenantName.from(t)))
+ .orElseGet(() -> controller.notificationsDb().listTenantsWithNotifications().stream())
+ .flatMap(tenantName -> controller.notificationsDb().listNotifications(NotificationSource.from(tenantName), productionOnly).stream())
+ .filter(notification ->
+ propertyEquals(request, "application", ApplicationName::from, notification.source().application()) &&
+ propertyEquals(request, "instance", InstanceName::from, notification.source().instance()) &&
+ propertyEquals(request, "zone", ZoneId::from, notification.source().zoneId()) &&
+ propertyEquals(request, "job", JobType::fromJobName, notification.source().jobType()) &&
+ propertyEquals(request, "type", Notification.Type::valueOf, Optional.of(notification.type())) &&
+ propertyEquals(request, "level", Notification.Level::valueOf, Optional.of(notification.level())))
+ .forEach(notification -> toSlime(notificationsArray.addObject(), notification, includeTenantFieldInResponse, excludeMessages));
return new SlimeJsonResponse(slime);
}
+ private static <T> boolean propertyEquals(HttpRequest request, String property, Function<String, T> mapper, Optional<T> value) {
+ return Optional.ofNullable(request.getProperty(property))
+ .map(propertyValue -> value.isPresent() && mapper.apply(propertyValue).equals(value.get()))
+ .orElse(true);
+ }
- private static void toSlime(Cursor cursor, Notification notification) {
+ private static void toSlime(Cursor cursor, Notification notification, boolean includeTenantFieldInResponse, boolean excludeMessages) {
cursor.setLong("at", notification.at().toEpochMilli());
cursor.setString("level", notificationLevelAsString(notification.level()));
cursor.setString("type", notificationTypeAsString(notification.type()));
- Cursor messagesArray = cursor.setArray("messages");
- notification.messages().forEach(messagesArray::addString);
+ if (!excludeMessages) {
+ Cursor messagesArray = cursor.setArray("messages");
+ notification.messages().forEach(messagesArray::addString);
+ }
+ if (includeTenantFieldInResponse) cursor.setString("tenant", notification.source().tenant().value());
notification.source().application().ifPresent(application -> cursor.setString("application", application.value()));
notification.source().instance().ifPresent(instance -> cursor.setString("instance", instance.value()));
notification.source().zoneId().ifPresent(zoneId -> {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
index 516911b3c7b..61e9ea02acc 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -861,6 +861,10 @@ public class ApplicationApiTest extends ControllerContainerTest {
"");
addNotifications(TenantName.from("tenant1"));
+ addNotifications(TenantName.from("tenant2"));
+ tester.assertResponse(request("/application/v4/notifications", GET)
+ .properties(Map.of("type", "applicationPackage", "excludeMessages", "true")).userIdentity(HOSTED_VESPA_OPERATOR),
+ new File("notifications-applicationPackage.json"));
tester.assertResponse(request("/application/v4/tenant/tenant1/notifications", GET).userIdentity(USER_ID),
new File("notifications-tenant1.json"));
tester.assertResponse(request("/application/v4/tenant/tenant1/notifications", GET)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json
new file mode 100644
index 00000000000..c0833ae0f05
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json
@@ -0,0 +1,18 @@
+{
+ "notifications": [
+ {
+ "at": "(ignore)",
+ "level": "warning",
+ "type": "applicationPackage",
+ "tenant": "tenant1",
+ "application": "app1"
+ },
+ {
+ "at": "(ignore)",
+ "level": "warning",
+ "type": "applicationPackage",
+ "tenant": "tenant2",
+ "application": "app1"
+ }
+ ]
+}