summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2023-10-20 16:48:15 +0200
committerGitHub <noreply@github.com>2023-10-20 16:48:15 +0200
commit2f60ec76050d005a04dc20b09ab7877bdd3abfb5 (patch)
treeaa386491ca2f7fb1441b71513eb325e13da26c86 /controller-server
parent6bd0004b1ee49c7c97d0fd2cd2ecfc85148275b5 (diff)
parent12b1d52427cc3decb98f24f24739affe4fc3fb09 (diff)
Merge pull request #29053 from vespa-engine/bjorncs/email-notification
Bjorncs/email notification
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java117
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java25
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java73
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java1
-rw-r--r--controller-server/src/main/resources/mail/mail-verification.vm (renamed from controller-server/src/main/resources/mail/mail-verification.tmpl)8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json2
15 files changed, 181 insertions, 110 deletions
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 d46f59f36a7..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)
@@ -171,7 +172,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/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<Template> fromId(String id) {
+ return Arrays.stream(values()).filter(t -> t.id.equals(id)).findAny();
+ }
+
+ private final String id;
+
+ Template(String id) { this.id = id; }
+
+ public String getId() { return id; }
+ }
+
+ private final VelocityEngine velocity;
+ private final EscapeTool escapeTool = new EscapeTool();
+ private final URI dashboardUri;
+
+ public MailTemplating(ZoneRegistry zoneRegistry) {
+ this.velocity = createTemplateEngine();
+ this.dashboardUri = zoneRegistry.dashboardUrl();
+ }
+
+ public String generateDefaultMailHtml(Template mailBodyTemplate, Map<String, Object> params, TenantName tenant) {
+ var ctx = createVelocityContext();
+ ctx.put("accountNotificationLink", accountNotificationsUri(tenant));
+ ctx.put("privacyPolicyLink", "https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html");
+ ctx.put("termsOfServiceLink", consoleUri("terms-of-service-trial.html"));
+ ctx.put("supportLink", consoleUri("support"));
+ ctx.put("mailBodyTemplate", mailBodyTemplate.getId());
+ params.forEach(ctx::put);
+ return render(ctx, Template.MAIL);
+ }
+
+ public String generateMailVerificationHtml(PendingMailVerification pmf) {
+ var ctx = createVelocityContext();
+ ctx.put("consoleLink", dashboardUri.getHost());
+ ctx.put("email", pmf.getMailAddress());
+ ctx.put("code", pmf.getVerificationCode());
+ return render(ctx, Template.MAIL_VERIFICATION);
+ }
+
+ public String escapeHtml(String s) { return escapeTool.html(s); }
+
+ private VelocityContext createVelocityContext() {
+ var ctx = new VelocityContext();
+ ctx.put("esc", escapeTool);
+ return ctx;
+ }
+
+ private String render(VelocityContext ctx, Template template) {
+ var writer = new StringWriter();
+ // Ignoring return value - implementation either returns 'true' or throws, never 'false'
+ velocity.mergeTemplate(template.getId(), StandardCharsets.UTF_8.name(), ctx, writer);
+ return writer.toString();
+ }
+
+ private static VelocityEngine createTemplateEngine() {
+ var v = new VelocityEngine();
+ v.setProperty(Velocity.RESOURCE_LOADERS, "string");
+ v.setProperty(Velocity.RESOURCE_LOADER + ".string.class", StringResourceLoader.class.getName());
+ v.setProperty(Velocity.RESOURCE_LOADER + ".string.repository.static", "false");
+ v.init();
+ var repo = (StringResourceRepository) v.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT);
+ Arrays.stream(Template.values()).forEach(t -> registerTemplate(repo, t.getId()));
+ return v;
+ }
+
+ private static void registerTemplate(StringResourceRepository repo, String name) {
+ var templateStr = Exceptions.uncheck(() -> {
+ var in = MailTemplating.class.getResourceAsStream("/mail/%s.vm".formatted(name));
+ return new String(in.readAllBytes());
+ });
+ repo.putStringResource(name, templateStr);
+ }
+
+ private String accountNotificationsUri(TenantName tenant) {
+ return new UriBuilder(dashboardUri)
+ .append("tenant/")
+ .append(tenant.value())
+ .append("account/notifications")
+ .toString();
+ }
+
+ private String consoleUri(String path) {
+ return new UriBuilder(dashboardUri).append(path).toString();
+ }
+}
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..4a94098ce98 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<String> messages, Optional<MailContent> mailContent) {
+ String title, List<String> messages, Optional<MailContent> mailContent) {
+
+ public Notification(Instant at, Type type, Level level, NotificationSource source, String title, List<String> messages) {
+ this(at, type, level, source, title, messages, Optional.empty());
+ }
public Notification(Instant at, Type type, Level level, NotificationSource source, List<String> 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);
}
@@ -81,7 +90,7 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve
}
public static class MailContent {
- private final String template;
+ private final MailTemplating.Template template;
private final SortedMap<String, Object> values;
private final String subject;
@@ -91,18 +100,18 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve
subject = b.subject;
}
- public String template() { return template; }
+ public MailTemplating.Template template() { return template; }
public SortedMap<String, Object> values() { return Collections.unmodifiableSortedMap(values); }
public Optional<String> subject() { return Optional.ofNullable(subject); }
- public static Builder fromTemplate(String template) { return new Builder(template); }
+ public static Builder fromTemplate(MailTemplating.Template template) { return new Builder(template); }
public static class Builder {
- private final String template;
+ private final MailTemplating.Template template;
private final Map<String, Object> values = new HashMap<>();
private String subject;
- private Builder(String template) {
+ private Builder(MailTemplating.Template template) {
this.template = template;
}
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<String> 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<String> messages,
+ public void setNotification(NotificationSource source, Type type, Level level, String title, List<String> messages,
Optional<MailContent> mailContent) {
Optional<Notification> changed = Optional.empty();
try (Mutex lock = curatorDb.lockNotifications(source.tenant())) {
@@ -80,7 +80,7 @@ public class NotificationsDb {
List<Notification> 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<Notification> 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/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
index 6468a4c397b..e3bfb8b4c56 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
@@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.notification;
import com.google.common.annotations.VisibleForTesting;
import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.TenantName;
import com.yahoo.restapi.UriBuilder;
import com.yahoo.text.Text;
import com.yahoo.vespa.flags.FetchVector;
@@ -16,17 +15,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-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.Collection;
import java.util.List;
import java.util.Objects;
@@ -46,7 +36,7 @@ public class Notifier {
private final FlagSource flagSource;
private final NotificationFormatter formatter;
private final URI dashboardUri;
- private final VelocityEngine velocity;
+ private final MailTemplating mailTemplating;
private static final Logger log = Logger.getLogger(Notifier.class.getName());
@@ -59,29 +49,7 @@ public class Notifier {
this.flagSource = Objects.requireNonNull(flagSource);
this.formatter = new NotificationFormatter(zoneRegistry);
this.dashboardUri = zoneRegistry.dashboardUrl();
- this.velocity = createTemplateEngine();
- }
-
- private static VelocityEngine createTemplateEngine() {
- var v = new VelocityEngine();
- v.setProperty(Velocity.RESOURCE_LOADERS, "string");
- v.setProperty(Velocity.RESOURCE_LOADER + ".string.class", StringResourceLoader.class.getName());
- v.setProperty(Velocity.RESOURCE_LOADER + ".string.repository.static", "false");
- v.init();
- var repo = (StringResourceRepository) v.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT);
- registerTemplate(repo, "mail");
- registerTemplate(repo, "default-mail-content");
- registerTemplate(repo, "notification-message");
- registerTemplate(repo, "cloud-trial-notification");
- return v;
- }
-
- private static void registerTemplate(StringResourceRepository repo, String name) {
- var templateStr = Exceptions.uncheck(() -> {
- var in = Notifier.class.getResourceAsStream("/mail/%s.vm".formatted(name));
- return new String(in.readAllBytes());
- });
- repo.putStringResource(name, templateStr);
+ this.mailTemplating = new MailTemplating(zoneRegistry);
}
public void dispatch(List<Notification> notifications, NotificationSource source) {
@@ -154,26 +122,13 @@ public class Notifier {
}
private String generateHtml(FormattedNotification content) {
- var esc = new EscapeTool();
- var mailContent = content.notification().mailContent().orElseGet(() -> generateContentFromMessages(content, esc));
- var ctx = new VelocityContext();
- ctx.put("esc", esc);
- ctx.put("accountNotificationLink", accountNotificationsUri(content.notification().source().tenant()));
- ctx.put("privacyPolicyLink", "https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html");
- ctx.put("termsOfServiceLink", consoleUri("terms-of-service-trial.html"));
- ctx.put("supportLink", consoleUri("support"));
- ctx.put("mailBodyTemplate", mailContent.template());
- mailContent.values().forEach(ctx::put);
-
- var writer = new StringWriter();
- // Ignoring return value - implementation either returns 'true' or throws, never 'false'
- velocity.mergeTemplate("mail", StandardCharsets.UTF_8.name(), ctx, writer);
- return writer.toString();
+ var mailContent = content.notification().mailContent().orElseGet(() -> generateContentFromMessages(content));
+ return mailTemplating.generateDefaultMailHtml(mailContent.template(), mailContent.values(), content.notification().source().tenant());
}
- private Notification.MailContent generateContentFromMessages(FormattedNotification f, EscapeTool esc) {
- var items = f.notification().messages().stream().map(m -> capitalise(linkify(esc.html(m)))).toList();
- return Notification.MailContent.fromTemplate("default-mail-content")
+ private Notification.MailContent generateContentFromMessages(FormattedNotification f) {
+ var items = f.notification().messages().stream().map(m -> capitalise(linkify(mailTemplating.escapeHtml(m)))).toList();
+ return Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT)
.with("mailMessageTemplate", "notification-message")
.with("mailTitle", "Vespa Cloud Notifications")
.with("notificationHeader", f.messagePrefix())
@@ -195,18 +150,6 @@ public class Notifier {
return sb.toString();
}
- private String accountNotificationsUri(TenantName tenant) {
- return new UriBuilder(dashboardUri)
- .append("tenant/")
- .append(tenant.value())
- .append("account/notifications")
- .toString();
- }
-
- private String consoleUri(String path) {
- return new UriBuilder(dashboardUri).append(path).toString();
- }
-
private String notificationLink(NotificationSource source) {
var uri = new UriBuilder(dashboardUri);
uri = uri.append("tenant").append(source.tenant().value());
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..d5be4d22dc2 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
@@ -12,6 +12,7 @@ import com.yahoo.slime.ObjectTraverser;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
@@ -36,6 +37,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 +55,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);
@@ -64,7 +67,7 @@ public class NotificationsSerializer {
notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber));
notification.mailContent().ifPresent(mc -> {
- notificationObject.setString("mail-template", mc.template());
+ notificationObject.setString("mail-template", mc.template().getId());
mc.subject().ifPresent(s -> notificationObject.setString("mail-subject", s));
var mailParamsCursor = notificationObject.setObject("mail-params");
mc.values().forEach((key, value) -> {
@@ -110,15 +113,15 @@ 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));
}
private Optional<Notification.MailContent> mailContentFrom(final Inspector inspector) {
return SlimeUtils.optionalString(inspector.field("mail-template")).map(template -> {
- var builder = Notification.MailContent.fromTemplate(template);
+ var builder = Notification.MailContent.fromTemplate(MailTemplating.Template.fromId(template).orElseThrow());
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());
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/main/resources/mail/mail-verification.tmpl b/controller-server/src/main/resources/mail/mail-verification.vm
index 8a473e74755..6905a292ee7 100644
--- a/controller-server/src/main/resources/mail/mail-verification.tmpl
+++ b/controller-server/src/main/resources/mail/mail-verification.vm
@@ -366,7 +366,7 @@
"
>
<p style="margin: 10px 0; text-align: center">
- You have entered the email address <b>%{email}</b> in
+ You have entered the email address <b>$esc.html($email)</b> in
Vespa Cloud.&nbsp;
</p>
<p style="margin: 10px 0; text-align: center">
@@ -411,7 +411,7 @@
valign="middle"
>
<a
- href="https://%{consoleUrl}/verify?code=%{code}"
+ href="https://$consoleUrl/verify?code=$code"
style="
display: inline-block;
background: #3b9fde;
@@ -471,9 +471,9 @@
<a
target="_blank"
rel="noopener noreferrer"
- href="https://%{consoleUrl}/verify?code=%{code}"
+ href="https://$consoleUrl/verify?code=$code"
style="color: #3b9fde"
- >https://%{consoleUrl}/verify?code=%{code}</a
+ >https://$consoleUrl/verify?code=$code</a
>
</p>
</div>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java
index ca71b912feb..a7af0916e59 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java
@@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
import com.yahoo.vespa.hosted.controller.application.MailVerifier;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
@@ -16,11 +15,9 @@ import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import java.net.URI;
import java.time.Duration;
import java.util.List;
-import static com.yahoo.yolean.Exceptions.uncheck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -33,7 +30,7 @@ class MailVerifierTest {
private final ControllerTester tester = new ControllerTester(SystemName.Public);
private final MockMailer mailer = tester.serviceRegistry().mailer();
- private final MailVerifier mailVerifier = new MailVerifier(URI.create("https://dashboard.uri.example.com"), tester.controller().tenants(), mailer, tester.curator(), tester.clock());
+ private final MailVerifier mailVerifier = new MailVerifier(tester.zoneRegistry(), tester.controller().tenants(), mailer, tester.curator(), tester.clock());
private static final TenantName tenantName = TenantName.from("scoober");
private static final String mail = "unverified@bar.com";
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..65da43a3ec4 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
@@ -8,6 +8,7 @@ import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
+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 org.junit.jupiter.api.Test;
@@ -28,7 +29,7 @@ public class NotificationsSerializerTest {
void serialization_test() throws IOException {
NotificationsSerializer serializer = new NotificationsSerializer();
TenantName tenantName = TenantName.from("tenant1");
- var mail = Notification.MailContent.fromTemplate("my-template").subject("My mail subject")
+ var mail = Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT).subject("My mail subject")
.with("string-param", "string-value").with("list-param", List.of("elem1", "elem2")).build();
List<Notification> notifications = List.of(
new Notification(Instant.ofEpochSecond(1234),
@@ -40,7 +41,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,18 +50,20 @@ 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\"," +
"\"runNumber\":12," +
- "\"mail-template\":\"my-template\"," +
+ "\"mail-template\":\"default-mail-content\"," +
"\"mail-subject\":\"My mail subject\"," +
"\"mail-params\":{\"list-param\":[\"elem1\",\"elem2\"],\"string-param\":\"string-value\"}" +
"}]}", new String(SlimeUtils.toJsonBytes(serialized)));
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"
],