diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2023-10-20 16:48:15 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-20 16:48:15 +0200 |
commit | 2f60ec76050d005a04dc20b09ab7877bdd3abfb5 (patch) | |
tree | aa386491ca2f7fb1441b71513eb325e13da26c86 /controller-server | |
parent | 6bd0004b1ee49c7c97d0fd2cd2ecfc85148275b5 (diff) | |
parent | 12b1d52427cc3decb98f24f24739affe4fc3fb09 (diff) |
Merge pull request #29053 from vespa-engine/bjorncs/email-notification
Bjorncs/email notification
Diffstat (limited to 'controller-server')
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. </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" ], |