diff options
author | Bjørn Christian Seime <bjorncs@vespa.ai> | 2023-10-16 13:47:41 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@vespa.ai> | 2023-10-16 16:03:45 +0200 |
commit | ca8117d5128433efad292025c911814bd776361f (patch) | |
tree | 2466127a138e4e9efa74941fbb4ccc676ce88ef2 | |
parent | 5af33f2eb25475eabe3c8136920104e3bf25907e (diff) |
Include email content when persisting notification
4 files changed, 77 insertions, 9 deletions
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 40c24c6f339..5116ecaf053 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 @@ -2,11 +2,15 @@ package com.yahoo.vespa.hosted.controller.notification; import java.time.Instant; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; /** * Represents an event that we want to notify the tenant about. The message(s) should be short @@ -78,34 +82,57 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve public static class MailContent { private final String template; - private final Map<String, Object> values; + private final SortedMap<String, Object> values; private final String subject; private MailContent(Builder b) { template = Objects.requireNonNull(b.template); - values = Map.copyOf(b.values); + values = new TreeMap<>(b.values); subject = b.subject; } public String template() { return template; } - public Map<String, Object> values() { return Map.copyOf(values); } + 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 class Builder { private final String template; - private final HashMap<String, Object> values = new HashMap<>(); + private final Map<String, Object> values = new HashMap<>(); private String subject; private Builder(String template) { this.template = template; } - public Builder with(String name, Object value) { values.put(name, value); return this; } + public Builder with(String name, String value) { values.put(name, value); return this; } + public Builder with(String name, Collection<String> items) { values.put(name, List.copyOf(items)); return this; } public Builder subject(String s) { this.subject = s; return this; } public MailContent build() { return new MailContent(this); } } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MailContent that = (MailContent) o; + return Objects.equals(template, that.template) && Objects.equals(values, that.values) && Objects.equals(subject, that.subject); + } + + @Override + public int hashCode() { + return Objects.hash(template, values, subject); + } + + @Override + public String toString() { + return "MailContent{" + + "template='" + template + '\'' + + ", values=" + values + + ", subject='" + subject + '\'' + + '}'; + } } } 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 e752e13eddd..a5d26feafaa 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 @@ -71,7 +71,6 @@ public class NotificationsDb { /** * 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. - * Email content is not persisted here. The email dispatcher is responsible for reliable delivery. */ public void setNotification(NotificationSource source, Type type, Level level, List<String> messages, Optional<MailContent> mailContent) { 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 7915a833be6..62b35d4cfd4 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 @@ -8,6 +8,7 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; +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; @@ -15,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.notification.Notification; import com.yahoo.vespa.hosted.controller.notification.NotificationSource; import java.util.List; +import java.util.Optional; /** * (de)serializes notifications for a tenant @@ -60,6 +62,22 @@ public class NotificationsSerializer { notification.source().clusterId().ifPresent(clusterId -> notificationObject.setString(clusterIdField, clusterId.value())); notification.source().jobType().ifPresent(jobType -> notificationObject.setString(jobTypeField, jobType.serialized())); notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber)); + + notification.mailContent().ifPresent(mc -> { + notificationObject.setString("mail-template", mc.template()); + mc.subject().ifPresent(s -> notificationObject.setString("mail-subject", s)); + var mailParamsCursor = notificationObject.setObject("mail-params"); + mc.values().forEach((key, value) -> { + if (value instanceof String str) { + mailParamsCursor.setString(key, str); + } else if (value instanceof List<?> l) { + var array = mailParamsCursor.setArray(key); + l.forEach(elem -> array.addString((String) elem)); + } else { + throw new ClassCastException("Unsupported param type: " + value.getClass()); + } + }); + }); } return slime; @@ -92,7 +110,24 @@ 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.entriesStream(inspector.field(messagesField)).map(Inspector::asString).toList()); + 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); + 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()); + case ARRAY -> builder.with(name, SlimeUtils.entriesStream(insp).map(Inspector::asString).toList()); + default -> throw new IllegalArgumentException("Unsupported param type: " + insp.type()); + } + }); + return builder.build(); + }); } private static String asString(Notification.Type type) { 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 63aa45a5a34..26eb30b6525 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 @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; import java.time.Instant; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,6 +28,8 @@ 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") + .with("string-param", "string-value").with("list-param", List.of("elem1", "elem2")).build(); List<Notification> notifications = List.of( new Notification(Instant.ofEpochSecond(1234), Notification.Type.applicationPackage, @@ -37,7 +40,8 @@ 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"))); + List.of("Failed to deploy: Node allocation failure"), + Optional.of(mail))); Slime serialized = serializer.toSlime(notifications); assertEquals("{\"notifications\":[" + @@ -55,7 +59,10 @@ public class NotificationsSerializerTest { "\"application\":\"app1\"," + "\"instance\":\"instance1\"," + "\"jobId\":\"test.us-east-1\"," + - "\"runNumber\":12" + + "\"runNumber\":12," + + "\"mail-template\":\"my-template\"," + + "\"mail-subject\":\"My mail subject\"," + + "\"mail-params\":{\"list-param\":[\"elem1\",\"elem2\"],\"string-param\":\"string-value\"}" + "}]}", new String(SlimeUtils.toJsonBytes(serialized))); List<Notification> deserialized = serializer.fromSlime(tenantName, serialized); |