feat(m28+m29+m30): ACME ARI, email digest, and Helm chart

M28: ACME Renewal Information (RFC 9702) — CA-directed renewal timing
with cert ID computation, directory endpoint discovery, graceful
degradation for non-ARI CAs. 19 tests.

M29: Email notifier wiring + scheduled certificate digest — SMTP
connector bridged to service layer via NotifierAdapter, DigestService
with HTML email template, 7th scheduler loop (24h), digest preview/send
API endpoints and GUI card. 21 tests.

M30: Production-ready Helm chart — server Deployment, PostgreSQL
StatefulSet, agent DaemonSet, ConfigMaps, Secrets, Ingress, security
contexts, health probes, example values for dev/prod/ACME scenarios.

Also: OpenAPI spec updates, MCP tool additions, CI helm-lint job,
documentation updates across 5 doc files and README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-28 21:18:35 -04:00
parent cb2ef9d0e7
commit ec21c9bb29
61 changed files with 6106 additions and 27 deletions
@@ -0,0 +1,42 @@
package email
import (
"context"
"fmt"
)
// NotifierAdapter bridges the email.Connector (notifier.Connector interface) to the
// service.Notifier interface used by the notification registry. This adapter allows
// the existing email SMTP connector to be registered alongside Slack, Teams, etc.
type NotifierAdapter struct {
connector *Connector
}
// NewNotifierAdapter wraps an email.Connector to implement service.Notifier.
func NewNotifierAdapter(c *Connector) *NotifierAdapter {
return &NotifierAdapter{connector: c}
}
// Channel returns the notification channel identifier.
func (a *NotifierAdapter) Channel() string {
return "Email"
}
// Send delivers a notification via SMTP email.
// The recipient is the email address, subject is used as the email subject,
// and body is the email body content.
func (a *NotifierAdapter) Send(ctx context.Context, recipient string, subject string, body string) error {
if recipient == "" {
return fmt.Errorf("email: recipient address is required")
}
return a.connector.sendEmail(ctx, recipient, subject, body)
}
// SendHTML delivers an HTML email notification via SMTP.
// Used by the digest service for rich HTML digest emails.
func (a *NotifierAdapter) SendHTML(ctx context.Context, recipient string, subject string, htmlBody string) error {
if recipient == "" {
return fmt.Errorf("email: recipient address is required")
}
return a.connector.sendHTMLEmail(ctx, recipient, subject, htmlBody)
}
@@ -0,0 +1,47 @@
package email
import (
"context"
"testing"
)
func TestNotifierAdapter_Channel(t *testing.T) {
connector := New(&Config{
SMTPHost: "smtp.example.com",
SMTPPort: 587,
FromAddress: "test@example.com",
}, nil)
adapter := NewNotifierAdapter(connector)
if adapter.Channel() != "Email" {
t.Errorf("expected channel 'Email', got '%s'", adapter.Channel())
}
}
func TestNotifierAdapter_Send_EmptyRecipient(t *testing.T) {
connector := New(&Config{
SMTPHost: "smtp.example.com",
SMTPPort: 587,
FromAddress: "test@example.com",
}, nil)
adapter := NewNotifierAdapter(connector)
err := adapter.Send(context.Background(), "", "test subject", "test body")
if err == nil {
t.Fatal("expected error for empty recipient")
}
}
func TestNotifierAdapter_SendHTML_EmptyRecipient(t *testing.T) {
connector := New(&Config{
SMTPHost: "smtp.example.com",
SMTPPort: 587,
FromAddress: "test@example.com",
}, nil)
adapter := NewNotifierAdapter(connector)
err := adapter.SendHTML(context.Background(), "", "test subject", "<html>test</html>")
if err == nil {
t.Fatal("expected error for empty recipient")
}
}
@@ -195,6 +195,73 @@ func (c *Connector) sendEmail(ctx context.Context, to, subject, body string) err
return nil
}
// sendHTMLEmail sends an HTML email message using the configured SMTP server.
// Used by the digest service for rich HTML digest emails.
func (c *Connector) sendHTMLEmail(ctx context.Context, to, subject, htmlBody string) error {
addr := net.JoinHostPort(c.config.SMTPHost, strconv.Itoa(c.config.SMTPPort))
var auth smtp.Auth
if c.config.Username != "" && c.config.Password != "" {
auth = smtp.PlainAuth("", c.config.Username, c.config.Password, c.config.SMTPHost)
}
var conn net.Conn
var err error
if c.config.UseTLS {
tlsConfig := &tls.Config{
ServerName: c.config.SMTPHost,
}
conn, err = tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return fmt.Errorf("failed to connect via TLS: %w", err)
}
} else {
conn, err = net.Dial("tcp", addr)
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
}
defer conn.Close()
client, err := smtp.NewClient(conn, c.config.SMTPHost)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
defer client.Close()
if auth != nil {
if err := client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
}
if err := client.Mail(c.config.FromAddress); err != nil {
return fmt.Errorf("failed to set sender: %w", err)
}
if err := client.Rcpt(to); err != nil {
return fmt.Errorf("failed to set recipient: %w", err)
}
wc, err := client.Data()
if err != nil {
return fmt.Errorf("failed to get data writer: %w", err)
}
defer wc.Close()
message := c.formatHTMLEmailMessage(c.config.FromAddress, to, subject, htmlBody)
if _, err := wc.Write(message); err != nil {
return fmt.Errorf("failed to write message: %w", err)
}
if err := client.Quit(); err != nil {
return fmt.Errorf("failed to quit SMTP: %w", err)
}
return nil
}
// formatEmailMessage formats an email message with standard headers.
func (c *Connector) formatEmailMessage(from, to, subject, body string) []byte {
message := fmt.Sprintf(
@@ -208,6 +275,19 @@ func (c *Connector) formatEmailMessage(from, to, subject, body string) []byte {
return []byte(message)
}
// formatHTMLEmailMessage formats an HTML email message with MIME headers.
func (c *Connector) formatHTMLEmailMessage(from, to, subject, htmlBody string) []byte {
message := fmt.Sprintf(
"From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\n\r\n%s",
from,
to,
subject,
time.Now().Format(time.RFC1123Z),
htmlBody,
)
return []byte(message)
}
// formatAlertBody formats an alert notification as email body text.
func (c *Connector) formatAlertBody(alert notifier.Alert) string {
body := fmt.Sprintf(`