Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit297089e

Browse files
defelmnqdefelmnq
and
defelmnq
authored
feat(coderd): add company logo when available for email notifications (#14935)
This PR aims toclose#14253 We keep the default behavior using the Coder logo if there's no logoset.Otherwise we want to use the logo based on the URL set in appearance.---------Co-authored-by: defelmnq <yvincent@coder.com>
1 parentc42f487 commit297089e

16 files changed

+289
-52
lines changed

‎coderd/notifications/dispatch/smtp.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,13 @@ type SMTPHandler struct {
5555

5656
noAuthWarnOnce sync.Once
5757
loginWarnOnce sync.Once
58-
59-
helpers template.FuncMap
6058
}
6159

62-
funcNewSMTPHandler(cfg codersdk.NotificationsEmailConfig,helpers template.FuncMap,log slog.Logger)*SMTPHandler {
63-
return&SMTPHandler{cfg:cfg,helpers:helpers,log:log}
60+
funcNewSMTPHandler(cfg codersdk.NotificationsEmailConfig,log slog.Logger)*SMTPHandler {
61+
return&SMTPHandler{cfg:cfg,log:log}
6462
}
6563

66-
func (s*SMTPHandler)Dispatcher(payload types.MessagePayload,titleTmpl,bodyTmplstring) (DeliveryFunc,error) {
64+
func (s*SMTPHandler)Dispatcher(payload types.MessagePayload,titleTmpl,bodyTmplstring,helpers template.FuncMap) (DeliveryFunc,error) {
6765
// First render the subject & body into their own discrete strings.
6866
subject,err:=markdown.PlaintextFromMarkdown(titleTmpl)
6967
iferr!=nil {
@@ -79,12 +77,12 @@ func (s *SMTPHandler) Dispatcher(payload types.MessagePayload, titleTmpl, bodyTm
7977
// Then, reuse these strings in the HTML & plain body templates.
8078
payload.Labels["_subject"]=subject
8179
payload.Labels["_body"]=htmlBody
82-
htmlBody,err=render.GoTemplate(htmlTemplate,payload,s.helpers)
80+
htmlBody,err=render.GoTemplate(htmlTemplate,payload,helpers)
8381
iferr!=nil {
8482
returnnil,xerrors.Errorf("render full html template: %w",err)
8583
}
8684
payload.Labels["_body"]=plainBody
87-
plainBody,err=render.GoTemplate(plainTemplate,payload,s.helpers)
85+
plainBody,err=render.GoTemplate(plainTemplate,payload,helpers)
8886
iferr!=nil {
8987
returnnil,xerrors.Errorf("render full plaintext template: %w",err)
9088
}

‎coderd/notifications/dispatch/smtp/html.gotmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<body style="margin: 0; padding: 0; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617; background: #f8fafc;">
99
<div style="max-width: 600px; margin: 20px auto; padding: 60px; border: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-align: left; font-size: 14px; line-height: 1.5;">
1010
<div style="text-align: center;">
11-
<img src="https://coder.com/coder-logo-horizontal.png" alt="Coder Logo" style="height: 40px;" />
11+
<img src="{{ logo_url }}" alt="{{ app_name }} Logo" style="height: 40px;" />
1212
</div>
1313
<h1 style="text-align: center; font-size: 24px; font-weight: 400; margin: 8px 0 32px; line-height: 1.5;">
1414
{{ .Labels._subject }}

‎coderd/notifications/dispatch/smtp_test.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -442,11 +442,7 @@ func TestSMTP(t *testing.T) {
442442
require.NoError(t,hp.Set(listen.Addr().String()))
443443
tc.cfg.Smarthost=hp
444444

445-
helpers:=map[string]any{
446-
"base_url":func()string {return"http://test.com" },
447-
"current_year":func()string {return"2024" },
448-
}
449-
handler:=dispatch.NewSMTPHandler(tc.cfg,helpers,logger.Named("smtp"))
445+
handler:=dispatch.NewSMTPHandler(tc.cfg,logger.Named("smtp"))
450446

451447
// Start mock SMTP server in the background.
452448
varwg sync.WaitGroup
@@ -484,7 +480,7 @@ func TestSMTP(t *testing.T) {
484480
Labels:make(map[string]string),
485481
}
486482

487-
dispatchFn,err:=handler.Dispatcher(payload,subject,body)
483+
dispatchFn,err:=handler.Dispatcher(payload,subject,body,helpers())
488484
require.NoError(t,err)
489485

490486
msgID:=uuid.New()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dispatch_test
2+
3+
funchelpers()map[string]any {
4+
returnmap[string]any{
5+
"base_url":func()string {return"http://test.com" },
6+
"current_year":func()string {return"2024" },
7+
"logo_url":func()string {return"https://coder.com/coder-logo-horizontal.png" },
8+
"app_name":func()string {return"Coder" },
9+
}
10+
}

‎coderd/notifications/dispatch/webhook.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"io"
99
"net/http"
10+
"text/template"
1011

1112
"github.com/google/uuid"
1213
"golang.org/x/xerrors"
@@ -41,7 +42,7 @@ func NewWebhookHandler(cfg codersdk.NotificationsWebhookConfig, log slog.Logger)
4142
return&WebhookHandler{cfg:cfg,log:log,cl:&http.Client{}}
4243
}
4344

44-
func (w*WebhookHandler)Dispatcher(payload types.MessagePayload,titleMarkdown,bodyMarkdownstring) (DeliveryFunc,error) {
45+
func (w*WebhookHandler)Dispatcher(payload types.MessagePayload,titleMarkdown,bodyMarkdownstring,_ template.FuncMap) (DeliveryFunc,error) {
4546
ifw.cfg.Endpoint.String()=="" {
4647
returnnil,xerrors.New("webhook endpoint not defined")
4748
}

‎coderd/notifications/dispatch/webhook_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func TestWebhook(t *testing.T) {
141141
Endpoint:*serpent.URLOf(endpoint),
142142
}
143143
handler:=dispatch.NewWebhookHandler(cfg,logger.With(slog.F("test",tc.name)))
144-
deliveryFn,err:=handler.Dispatcher(msgPayload,titleMarkdown,bodyMarkdown)
144+
deliveryFn,err:=handler.Dispatcher(msgPayload,titleMarkdown,bodyMarkdown,helpers())
145145
require.NoError(t,err)
146146

147147
retryable,err:=deliveryFn(ctx,msgID)

‎coderd/notifications/fetcher.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package notifications
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"text/template"
8+
9+
"golang.org/x/xerrors"
10+
11+
"cdr.dev/slog"
12+
)
13+
14+
func (n*notifier)fetchHelpers(ctx context.Context) (map[string]any,error) {
15+
appName,err:=n.fetchAppName(ctx)
16+
iferr!=nil {
17+
n.log.Error(ctx,"failed to fetch app name",slog.Error(err))
18+
returnnil,xerrors.Errorf("fetch app name: %w",err)
19+
}
20+
logoURL,err:=n.fetchLogoURL(ctx)
21+
iferr!=nil {
22+
n.log.Error(ctx,"failed to fetch logo URL",slog.Error(err))
23+
returnnil,xerrors.Errorf("fetch logo URL: %w",err)
24+
}
25+
26+
helpers:=make(template.FuncMap)
27+
fork,v:=rangen.helpers {
28+
helpers[k]=v
29+
}
30+
31+
helpers["app_name"]=func()string {returnappName }
32+
helpers["logo_url"]=func()string {returnlogoURL }
33+
34+
returnhelpers,nil
35+
}
36+
37+
func (n*notifier)fetchAppName(ctx context.Context) (string,error) {
38+
appName,err:=n.store.GetApplicationName(ctx)
39+
iferr!=nil {
40+
iferrors.Is(err,sql.ErrNoRows) {
41+
returnnotificationsDefaultAppName,nil
42+
}
43+
return"",xerrors.Errorf("get application name: %w",err)
44+
}
45+
returnappName,nil
46+
}
47+
48+
func (n*notifier)fetchLogoURL(ctx context.Context) (string,error) {
49+
logoURL,err:=n.store.GetLogoURL(ctx)
50+
iferr!=nil {
51+
iferrors.Is(err,sql.ErrNoRows) {
52+
returnnotificationsDefaultLogoURL,nil
53+
}
54+
return"",xerrors.Errorf("get logo URL: %w",err)
55+
}
56+
returnlogoURL,nil
57+
}

‎coderd/notifications/manager.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func NewManager(cfg codersdk.NotificationsConfig, store Store, helpers template.
109109
stop:make(chanany),
110110
done:make(chanany),
111111

112-
handlers:defaultHandlers(cfg,helpers,log),
112+
handlers:defaultHandlers(cfg,log),
113113
helpers:helpers,
114114

115115
clock:quartz.NewReal(),
@@ -121,9 +121,9 @@ func NewManager(cfg codersdk.NotificationsConfig, store Store, helpers template.
121121
}
122122

123123
// defaultHandlers builds a set of known handlers; panics if any error occurs as these handlers should be valid at compile time.
124-
funcdefaultHandlers(cfg codersdk.NotificationsConfig,helpers template.FuncMap,log slog.Logger)map[database.NotificationMethod]Handler {
124+
funcdefaultHandlers(cfg codersdk.NotificationsConfig,log slog.Logger)map[database.NotificationMethod]Handler {
125125
returnmap[database.NotificationMethod]Handler{
126-
database.NotificationMethodSmtp:dispatch.NewSMTPHandler(cfg.SMTP,helpers,log.Named("dispatcher.smtp")),
126+
database.NotificationMethodSmtp:dispatch.NewSMTPHandler(cfg.SMTP,log.Named("dispatcher.smtp")),
127127
database.NotificationMethodWebhook:dispatch.NewWebhookHandler(cfg.Webhook,log.Named("dispatcher.webhook")),
128128
}
129129
}

‎coderd/notifications/manager_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"sync/atomic"
77
"testing"
8+
"text/template"
89
"time"
910

1011
"github.com/google/uuid"
@@ -210,8 +211,8 @@ type santaHandler struct {
210211
nice atomic.Int32
211212
}
212213

213-
func (s*santaHandler)Dispatcher(payload types.MessagePayload,_,_string) (dispatch.DeliveryFunc,error) {
214-
returnfunc(ctx context.Context,msgID uuid.UUID) (retryablebool,errerror) {
214+
func (s*santaHandler)Dispatcher(payload types.MessagePayload,_,_string,_ template.FuncMap) (dispatch.DeliveryFunc,error) {
215+
returnfunc(_ context.Context,_ uuid.UUID) (retryablebool,errerror) {
215216
ifpayload.Labels["nice"]!="true" {
216217
s.naughty.Add(1)
217218
returnfalse,xerrors.New("be nice")

‎coderd/notifications/metrics_test.go

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strconv"
66
"sync"
77
"testing"
8+
"text/template"
89
"time"
910

1011
"github.com/google/uuid"
@@ -44,7 +45,7 @@ func TestMetrics(t *testing.T) {
4445

4546
reg:=prometheus.NewRegistry()
4647
metrics:=notifications.NewMetrics(reg)
47-
template:=notifications.TemplateWorkspaceDeleted
48+
tmpl:=notifications.TemplateWorkspaceDeleted
4849

4950
const (
5051
method=database.NotificationMethodSmtp
@@ -76,7 +77,7 @@ func TestMetrics(t *testing.T) {
7677
user:=createSampleUser(t,store)
7778

7879
// Build fingerprints for the two different series we expect.
79-
methodTemplateFP:=fingerprintLabels(notifications.LabelMethod,string(method),notifications.LabelTemplateID,template.String())
80+
methodTemplateFP:=fingerprintLabels(notifications.LabelMethod,string(method),notifications.LabelTemplateID,tmpl.String())
8081
methodFP:=fingerprintLabels(notifications.LabelMethod,string(method))
8182

8283
expected:=map[string]func(metric*dto.Metric,seriesstring)bool{
@@ -90,7 +91,7 @@ func TestMetrics(t *testing.T) {
9091

9192
varmatchstring
9293
forresult,val:=rangeresults {
93-
seriesFP:=fingerprintLabels(notifications.LabelMethod,string(method),notifications.LabelTemplateID,template.String(),notifications.LabelResult,result)
94+
seriesFP:=fingerprintLabels(notifications.LabelMethod,string(method),notifications.LabelTemplateID,tmpl.String(),notifications.LabelResult,result)
9495
if!hasMatchingFingerprint(metric,seriesFP) {
9596
continue
9697
}
@@ -165,9 +166,9 @@ func TestMetrics(t *testing.T) {
165166
}
166167

167168
// WHEN: 2 notifications are enqueued, 1 of which will fail until its retries are exhausted, and another which will succeed
168-
_,err=enq.Enqueue(ctx,user.ID,template,map[string]string{"type":"success"},"test")// this will succeed
169+
_,err=enq.Enqueue(ctx,user.ID,tmpl,map[string]string{"type":"success"},"test")// this will succeed
169170
require.NoError(t,err)
170-
_,err=enq.Enqueue(ctx,user.ID,template,map[string]string{"type":"failure"},"test2")// this will fail and retry (maxAttempts - 1) times
171+
_,err=enq.Enqueue(ctx,user.ID,tmpl,map[string]string{"type":"failure"},"test2")// this will fail and retry (maxAttempts - 1) times
171172
require.NoError(t,err)
172173

173174
mgr.Run(ctx)
@@ -218,7 +219,7 @@ func TestPendingUpdatesMetric(t *testing.T) {
218219

219220
reg:=prometheus.NewRegistry()
220221
metrics:=notifications.NewMetrics(reg)
221-
template:=notifications.TemplateWorkspaceDeleted
222+
tmpl:=notifications.TemplateWorkspaceDeleted
222223

223224
constmethod=database.NotificationMethodSmtp
224225

@@ -252,9 +253,9 @@ func TestPendingUpdatesMetric(t *testing.T) {
252253
user:=createSampleUser(t,store)
253254

254255
// WHEN: 2 notifications are enqueued, one of which will fail and one which will succeed
255-
_,err=enq.Enqueue(ctx,user.ID,template,map[string]string{"type":"success"},"test")// this will succeed
256+
_,err=enq.Enqueue(ctx,user.ID,tmpl,map[string]string{"type":"success"},"test")// this will succeed
256257
require.NoError(t,err)
257-
_,err=enq.Enqueue(ctx,user.ID,template,map[string]string{"type":"failure"},"test2")// this will fail and retry (maxAttempts - 1) times
258+
_,err=enq.Enqueue(ctx,user.ID,tmpl,map[string]string{"type":"failure"},"test2")// this will fail and retry (maxAttempts - 1) times
258259
require.NoError(t,err)
259260

260261
mgr.Run(ctx)
@@ -309,7 +310,7 @@ func TestInflightDispatchesMetric(t *testing.T) {
309310

310311
reg:=prometheus.NewRegistry()
311312
metrics:=notifications.NewMetrics(reg)
312-
template:=notifications.TemplateWorkspaceDeleted
313+
tmpl:=notifications.TemplateWorkspaceDeleted
313314

314315
constmethod=database.NotificationMethodSmtp
315316

@@ -342,7 +343,7 @@ func TestInflightDispatchesMetric(t *testing.T) {
342343

343344
// WHEN: notifications are enqueued which will succeed (and be delayed during dispatch)
344345
fori:=0;i<msgCount;i++ {
345-
_,err=enq.Enqueue(ctx,user.ID,template,map[string]string{"type":"success","i":strconv.Itoa(i)},"test")
346+
_,err=enq.Enqueue(ctx,user.ID,tmpl,map[string]string{"type":"success","i":strconv.Itoa(i)},"test")
346347
require.NoError(t,err)
347348
}
348349

@@ -351,7 +352,7 @@ func TestInflightDispatchesMetric(t *testing.T) {
351352
// THEN:
352353
// Ensure we see the dispatches of the messages inflight.
353354
require.Eventually(t,func()bool {
354-
returnpromtest.ToFloat64(metrics.InflightDispatches.WithLabelValues(string(method),template.String()))==msgCount
355+
returnpromtest.ToFloat64(metrics.InflightDispatches.WithLabelValues(string(method),tmpl.String()))==msgCount
355356
},testutil.WaitShort,testutil.IntervalFast)
356357

357358
fori:=0;i<msgCount;i++ {
@@ -389,7 +390,7 @@ func TestCustomMethodMetricCollection(t *testing.T) {
389390
var (
390391
reg=prometheus.NewRegistry()
391392
metrics=notifications.NewMetrics(reg)
392-
template=notifications.TemplateWorkspaceDeleted
393+
tmpl=notifications.TemplateWorkspaceDeleted
393394
anotherTemplate=notifications.TemplateWorkspaceDormant
394395
)
395396

@@ -400,7 +401,7 @@ func TestCustomMethodMetricCollection(t *testing.T) {
400401

401402
// GIVEN: a template whose notification method differs from the default.
402403
out,err:=store.UpdateNotificationTemplateMethodByID(ctx, database.UpdateNotificationTemplateMethodByIDParams{
403-
ID:template,
404+
ID:tmpl,
404405
Method: database.NullNotificationMethod{NotificationMethod:customMethod,Valid:true},
405406
})
406407
require.NoError(t,err)
@@ -426,7 +427,7 @@ func TestCustomMethodMetricCollection(t *testing.T) {
426427

427428
user:=createSampleUser(t,store)
428429

429-
_,err=enq.Enqueue(ctx,user.ID,template,map[string]string{"type":"success"},"test")
430+
_,err=enq.Enqueue(ctx,user.ID,tmpl,map[string]string{"type":"success"},"test")
430431
require.NoError(t,err)
431432
_,err=enq.Enqueue(ctx,user.ID,anotherTemplate,map[string]string{"type":"success"},"test")
432433
require.NoError(t,err)
@@ -447,7 +448,7 @@ func TestCustomMethodMetricCollection(t *testing.T) {
447448
// THEN: we should have metric series for both the default and custom notification methods.
448449
require.Eventually(t,func()bool {
449450
returnpromtest.ToFloat64(metrics.DispatchAttempts.WithLabelValues(string(defaultMethod),anotherTemplate.String(),notifications.ResultSuccess))>0&&
450-
promtest.ToFloat64(metrics.DispatchAttempts.WithLabelValues(string(customMethod),template.String(),notifications.ResultSuccess))>0
451+
promtest.ToFloat64(metrics.DispatchAttempts.WithLabelValues(string(customMethod),tmpl.String(),notifications.ResultSuccess))>0
451452
},testutil.WaitShort,testutil.IntervalFast)
452453
}
453454

@@ -525,8 +526,8 @@ func newBarrierHandler(total int, handler notifications.Handler) *barrierHandler
525526
}
526527
}
527528

528-
func (bh*barrierHandler)Dispatcher(payload types.MessagePayload,title,bodystring) (dispatch.DeliveryFunc,error) {
529-
deliverFn,err:=bh.h.Dispatcher(payload,title,body)
529+
func (bh*barrierHandler)Dispatcher(payload types.MessagePayload,title,bodystring,helpers template.FuncMap) (dispatch.DeliveryFunc,error) {
530+
deliverFn,err:=bh.h.Dispatcher(payload,title,body,helpers)
530531
iferr!=nil {
531532
returnnil,err
532533
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp