From d47230d3f2bbb29359791dbd32f700bd3578bea4 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Fri, 20 Oct 2023 15:27:57 +0200 Subject: Introduce 'title' to notification --- .../hosted/controller/maintenance/CloudTrialExpirer.java | 2 +- .../hosted/controller/notification/Notification.java | 15 ++++++++++++--- .../hosted/controller/notification/NotificationsDb.java | 10 +++++----- .../controller/persistence/NotificationsSerializer.java | 3 +++ .../restapi/application/ApplicationApiHandler.java | 1 + .../controller/maintenance/CloudTrialExpirerTest.java | 2 +- .../persistence/NotificationsSerializerTest.java | 6 ++++-- .../application/responses/notifications-tenant1-app2.json | 1 + .../application/responses/notifications-tenant1.json | 2 ++ 9 files changed, 30 insertions(+), 12 deletions(-) (limited to 'controller-server') diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java index d46f59f36a7..ba2e3099d5d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java @@ -171,7 +171,7 @@ public class CloudTrialExpirer extends ControllerMaintainer { // Remove previous notification to ensure new notification is sent by email controller().notificationsDb().removeNotification(source, Notification.Type.account); controller().notificationsDb().setNotification( - source, Notification.Type.account, Notification.Level.info, List.of(consoleMsg), mail); + source, Notification.Type.account, Notification.Level.info, consoleMsg, List.of(), mail); } private static TrialNotifications.Status updatedStatus(Tenant t, Instant i, TrialNotifications.State s) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java index 5116ecaf053..525a768457f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java @@ -21,10 +21,14 @@ import java.util.TreeMap; * @author freva */ public record Notification(Instant at, Notification.Type type, Notification.Level level, NotificationSource source, - List messages, Optional mailContent) { + String title, List messages, Optional mailContent) { + + public Notification(Instant at, Type type, Level level, NotificationSource source, String title, List messages) { + this(at, type, level, source, title, messages, Optional.empty()); + } public Notification(Instant at, Type type, Level level, NotificationSource source, List messages) { - this(at, type, level, source, messages, Optional.empty()); + this(at, type, level, source, "", messages); } public Notification { @@ -32,8 +36,13 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve type = Objects.requireNonNull(type, "type cannot be null"); level = Objects.requireNonNull(level, "level cannot be null"); source = Objects.requireNonNull(source, "source cannot be null"); + title = Objects.requireNonNull(title, "title cannot be null"); messages = List.copyOf(Objects.requireNonNull(messages, "messages cannot be null")); - if (messages.size() < 1) throw new IllegalArgumentException("messages cannot be empty"); + + // Allowing empty title temporarily until all notifications have a title + // if (title.isBlank()) throw new IllegalArgumentException("title cannot be empty"); + if (messages.isEmpty() && title.isBlank()) throw new IllegalArgumentException("messages cannot be empty when title is empty"); + mailContent = Objects.requireNonNull(mailContent); } 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 a5d26feafaa..081fd5a2c1d 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 @@ -65,14 +65,14 @@ public class NotificationsDb { } public void setNotification(NotificationSource source, Type type, Level level, List messages) { - setNotification(source, type, level, messages, Optional.empty()); + setNotification(source, type, level, "", messages, Optional.empty()); } /** * Add a notification with given source and type. If a notification with same source and type * already exists, it'll be replaced by this one instead. */ - public void setNotification(NotificationSource source, Type type, Level level, List messages, + public void setNotification(NotificationSource source, Type type, Level level, String title, List messages, Optional mailContent) { Optional changed = Optional.empty(); try (Mutex lock = curatorDb.lockNotifications(source.tenant())) { @@ -80,7 +80,7 @@ public class NotificationsDb { List notifications = existingNotifications.stream() .filter(notification -> !source.equals(notification.source()) || type != notification.type()) .collect(Collectors.toCollection(ArrayList::new)); - var notification = new Notification(clock.instant(), type, level, source, messages, mailContent); + var notification = new Notification(clock.instant(), type, level, source, title, messages, mailContent); if (!notificationExists(notification, existingNotifications, false)) { changed = Optional.of(notification); } @@ -190,7 +190,7 @@ public class NotificationsDb { .filter(status -> status.getFirst() == level) // Do not mix message from different levels .map(Pair::getSecond) .toList(); - return Optional.of(new Notification(at, Type.feedBlock, level, source, messages)); + return Optional.of(new Notification(at, Type.feedBlock, level, source, "", messages)); } private static Optional createReindexNotification(NotificationSource source, Instant at, Cluster cluster) { @@ -201,7 +201,7 @@ public class NotificationsDb { .sorted() .toList(); if (messages.isEmpty()) return Optional.empty(); - return Optional.of(new Notification(at, Type.reindex, Level.info, source, messages)); + return Optional.of(new Notification(at, Type.reindex, Level.info, source, "", messages)); } /** diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java index 62b35d4cfd4..b2c7b3db29e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java @@ -36,6 +36,7 @@ public class NotificationsSerializer { private static final String atFieldName = "at"; private static final String typeField = "type"; private static final String levelField = "level"; + private static final String titleField = "title"; private static final String messagesField = "messages"; private static final String applicationField = "application"; private static final String instanceField = "instance"; @@ -53,6 +54,7 @@ public class NotificationsSerializer { notificationObject.setLong(atFieldName, notification.at().toEpochMilli()); notificationObject.setString(typeField, asString(notification.type())); notificationObject.setString(levelField, asString(notification.level())); + notificationObject.setString(titleField, notification.title()); Cursor messagesArray = notificationObject.setArray(messagesField); notification.messages().forEach(messagesArray::addString); @@ -110,6 +112,7 @@ public class NotificationsSerializer { SlimeUtils.optionalString(inspector.field(clusterIdField)).map(ClusterSpec.Id::from), SlimeUtils.optionalString(inspector.field(jobTypeField)).map(jobName -> JobType.ofSerialized(jobName)), SlimeUtils.optionalLong(inspector.field(runNumberField))), + SlimeUtils.optionalString(inspector.field(titleField)).orElse(""), SlimeUtils.entriesStream(inspector.field(messagesField)).map(Inspector::asString).toList(), mailContentFrom(inspector)); } 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 fdde87074e9..03e6125a73a 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 @@ -1047,6 +1047,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { cursor.setString("level", notificationLevelAsString(notification.level())); cursor.setString("type", notificationTypeAsString(notification.type())); if (!excludeMessages) { + cursor.setString("title", notification.title()); Cursor messagesArray = cursor.setArray("messages"); notification.messages().forEach(messagesArray::addString); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java index 7e8237606c6..07ce2e415a7 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java @@ -173,7 +173,7 @@ public class CloudTrialExpirerTest { private String lastAccountLevelNotificationTitle(TenantName tenant) { return tester.controller().notificationsDb() .listNotifications(NotificationSource.from(tenant), false).stream() - .filter(n -> n.type() == Notification.Type.account).map(n -> n.messages().get(0)) + .filter(n -> n.type() == Notification.Type.account).map(Notification::title) .findFirst().orElseThrow(); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java index 26eb30b6525..6ea84f596ec 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java @@ -40,7 +40,7 @@ public class NotificationsSerializerTest { Notification.Type.deployment, Notification.Level.error, NotificationSource.from(new RunId(ApplicationId.from(tenantName.value(), "app1", "instance1"), DeploymentContext.systemTest, 12)), - List.of("Failed to deploy: Node allocation failure"), + "Failed to deploy", List.of("Node allocation failure"), Optional.of(mail))); Slime serialized = serializer.toSlime(notifications); @@ -49,13 +49,15 @@ public class NotificationsSerializerTest { "\"at\":1234000," + "\"type\":\"applicationPackage\"," + "\"level\":\"warning\"," + + "\"title\":\"\"," + "\"messages\":[\"Something something deprecated...\"]," + "\"application\":\"app1\"" + "},{" + "\"at\":2345000," + "\"type\":\"deployment\"," + "\"level\":\"error\"," + - "\"messages\":[\"Failed to deploy: Node allocation failure\"]," + + "\"title\":\"Failed to deploy\"," + + "\"messages\":[\"Node allocation failure\"]," + "\"application\":\"app1\"," + "\"instance\":\"instance1\"," + "\"jobId\":\"test.us-east-1\"," + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json index 556440c40d5..44ce2c510f9 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json @@ -4,6 +4,7 @@ "at": 1600000000000, "level": "error", "type": "deployment", + "title": "", "messages": [ "Failed to deploy: Node allocation failure" ], diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json index 1a731dfe4a9..dd8edfcc046 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json @@ -4,6 +4,7 @@ "at": 1600000000000, "level": "warning", "type": "applicationPackage", + "title": "", "messages": [ "Something something deprecated..." ], @@ -13,6 +14,7 @@ "at": 1600000000000, "level": "error", "type": "deployment", + "title": "", "messages": [ "Failed to deploy: Node allocation failure" ], -- cgit v1.2.3 From 15348a70192e827ee8c1df14ad22493bcae90570 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Fri, 20 Oct 2023 15:28:47 +0200 Subject: Remove dead code --- .../vespa/hosted/controller/persistence/NotificationsSerializer.java | 1 - 1 file changed, 1 deletion(-) (limited to 'controller-server') diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java index b2c7b3db29e..52766f699ac 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java @@ -121,7 +121,6 @@ public class NotificationsSerializer { return SlimeUtils.optionalString(inspector.field("mail-template")).map(template -> { var builder = Notification.MailContent.fromTemplate(template); SlimeUtils.optionalString(inspector.field("mail-subject")).ifPresent(builder::subject); - var paramsCursor = inspector.field("mail-params"); inspector.field("mail-params").traverse((ObjectTraverser) (name, insp) -> { switch (insp.type()) { case STRING -> builder.with(name, insp.asString()); -- cgit v1.2.3 From 12b1d52427cc3decb98f24f24739affe4fc3fb09 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Fri, 20 Oct 2023 15:49:28 +0200 Subject: Move out mail templating to separate class --- .../yahoo/vespa/hosted/controller/Controller.java | 2 +- .../controller/application/MailVerifier.java | 20 +- .../controller/maintenance/CloudTrialExpirer.java | 3 +- .../controller/notification/MailTemplating.java | 117 +++++ .../controller/notification/Notification.java | 10 +- .../hosted/controller/notification/Notifier.java | 73 +-- .../persistence/NotificationsSerializer.java | 5 +- .../src/main/resources/mail/mail-verification.tmpl | 494 --------------------- .../src/main/resources/mail/mail-verification.vm | 494 +++++++++++++++++++++ .../vespa/hosted/controller/MailVerifierTest.java | 5 +- .../persistence/NotificationsSerializerTest.java | 5 +- 11 files changed, 641 insertions(+), 587 deletions(-) create mode 100644 controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java delete mode 100644 controller-server/src/main/resources/mail/mail-verification.tmpl create mode 100644 controller-server/src/main/resources/mail/mail-verification.vm (limited to 'controller-server') diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java index 87885bc5f21..bab86e7bfde 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java @@ -134,7 +134,7 @@ public class Controller extends AbstractComponent { notifier = new Notifier(curator, serviceRegistry.zoneRegistry(), serviceRegistry.mailer(), flagSource); notificationsDb = new NotificationsDb(this); supportAccessControl = new SupportAccessControl(this); - mailVerifier = new MailVerifier(serviceRegistry.zoneRegistry().dashboardUrl(), tenantController, serviceRegistry.mailer(), curator, clock); + mailVerifier = new MailVerifier(serviceRegistry.zoneRegistry(), tenantController, serviceRegistry.mailer(), curator, clock); dataplaneTokenService = new DataplaneTokenService(this); // Record the version of this controller diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java index ecdfc5990c0..7d8f0d260fc 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java @@ -6,22 +6,21 @@ import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.TenantController; import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail; import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; +import com.yahoo.vespa.hosted.controller.notification.MailTemplating; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; +import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.TenantContacts; import com.yahoo.vespa.hosted.controller.tenant.TenantInfo; -import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; -import java.net.URI; import java.time.Clock; import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.UUID; -import static com.yahoo.yolean.Exceptions.uncheck; - /** * @author olaa @@ -34,14 +33,14 @@ public class MailVerifier { private final Mailer mailer; private final CuratorDb curatorDb; private final Clock clock; - private final URI dashboardUri; + private final MailTemplating mailTemplating; - public MailVerifier(URI dashboardUri, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) { + public MailVerifier(ZoneRegistry zoneRegistry, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) { this.tenantController = tenantController; this.mailer = mailer; this.curatorDb = curatorDb; this.clock = clock; - this.dashboardUri = dashboardUri; + this.mailTemplating = new MailTemplating(zoneRegistry); } public PendingMailVerification sendMailVerification(TenantName tenantName, String email, PendingMailVerification.MailType mailType) { @@ -133,12 +132,7 @@ public class MailVerifier { } private Mail mailOf(PendingMailVerification pendingMailVerification) { - var classLoader = this.getClass().getClassLoader(); - var template = uncheck(() -> classLoader.getResourceAsStream("mail/mail-verification.tmpl").readAllBytes()); - var message = new String(template) - .replaceAll("%\\{consoleUrl}", dashboardUri.getHost()) - .replaceAll("%\\{email}", pendingMailVerification.getMailAddress()) - .replaceAll("%\\{code}", pendingMailVerification.getVerificationCode()); + var message = mailTemplating.generateMailVerificationHtml(pendingMailVerification); return new Mail(List.of(pendingMailVerification.getMailAddress()), "Please verify your email", "", message); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java index ba2e3099d5d..d0426416349 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.flags.ListFlag; import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; +import com.yahoo.vespa.hosted.controller.notification.MailTemplating; import com.yahoo.vespa.hosted.controller.notification.Notification; import com.yahoo.vespa.hosted.controller.notification.NotificationSource; import com.yahoo.vespa.hosted.controller.persistence.TrialNotifications; @@ -160,7 +161,7 @@ public class CloudTrialExpirer extends ControllerMaintainer { } private void queueNotification(Tenant tenant, String consoleMsg, String emailSubject, String emailMsg) { - var mail = Optional.of(Notification.MailContent.fromTemplate("default-mail-content") + var mail = Optional.of(Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT) .subject(emailSubject) .with("mailMessageTemplate", "cloud-trial-notification") .with("cloudTrialMessage", emailMsg) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java new file mode 100644 index 00000000000..e8fb7289f4c --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java @@ -0,0 +1,117 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.controller.notification; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.restapi.UriBuilder; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; +import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; +import com.yahoo.yolean.Exceptions; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.runtime.resource.loader.StringResourceLoader; +import org.apache.velocity.runtime.resource.util.StringResourceRepository; +import org.apache.velocity.tools.generic.EscapeTool; + +import java.io.StringWriter; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +/** + * @author bjorncs + */ +public class MailTemplating { + + public enum Template { + MAIL("mail"), DEFAULT_MAIL_CONTENT("default-mail-content"), NOTIFICATION_MESSAGE("notification-message"), + CLOUD_TRIAL_NOTIFICATION("cloud-trial-notification"), MAIL_VERIFICATION("mail-verification"); + + public static Optional