Compare commits
2 Commits
628bba22fe
...
feature/un
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8d5a8f6cc | ||
|
|
73e01c7664 |
18
README.md
18
README.md
@@ -1,7 +1,6 @@
|
|||||||
# dnswatcher
|
# dnswatcher
|
||||||
|
|
||||||
> ⚠️ **Pre-1.0 software.** APIs, configuration, and behavior may change
|
> ⚠️ Pre-1.0 software. APIs, configuration, and behavior may change without notice.
|
||||||
> without notice.
|
|
||||||
|
|
||||||
dnswatcher is a production DNS and infrastructure monitoring daemon written in
|
dnswatcher is a production DNS and infrastructure monitoring daemon written in
|
||||||
Go. It watches configured DNS domains and hostnames for changes, monitors TCP
|
Go. It watches configured DNS domains and hostnames for changes, monitors TCP
|
||||||
@@ -19,19 +18,10 @@ without requiring an external database.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Target Classification
|
|
||||||
|
|
||||||
All monitored DNS names are provided via a single `DNSWATCHER_TARGETS`
|
|
||||||
list. dnswatcher uses the [Public Suffix List](https://publicsuffix.org/)
|
|
||||||
to automatically classify each entry as an apex domain (eTLD+1, e.g.
|
|
||||||
`example.com`, `example.co.uk`) or a hostname (subdomain, e.g.
|
|
||||||
`www.example.com`). Apex domains receive NS delegation monitoring;
|
|
||||||
hostnames receive per-nameserver record monitoring. Both receive port
|
|
||||||
and TLS checks.
|
|
||||||
|
|
||||||
### DNS Domain Monitoring (Apex Domains)
|
### DNS Domain Monitoring (Apex Domains)
|
||||||
|
|
||||||
- Apex domains are identified automatically via the PSL.
|
- Accepts a list of DNS domain names (apex domains, identified via the
|
||||||
|
[Public Suffix List](https://publicsuffix.org/)).
|
||||||
- Every **1 hour**, performs a full iterative trace from root servers to
|
- Every **1 hour**, performs a full iterative trace from root servers to
|
||||||
discover all authoritative nameservers (NS records) for each domain.
|
discover all authoritative nameservers (NS records) for each domain.
|
||||||
- Queries **every** discovered authoritative nameserver independently.
|
- Queries **every** discovered authoritative nameserver independently.
|
||||||
@@ -207,7 +197,7 @@ the following precedence (highest to lowest):
|
|||||||
| `PORT` | HTTP listen port | `8080` |
|
| `PORT` | HTTP listen port | `8080` |
|
||||||
| `DNSWATCHER_DEBUG` | Enable debug logging | `false` |
|
| `DNSWATCHER_DEBUG` | Enable debug logging | `false` |
|
||||||
| `DNSWATCHER_DATA_DIR` | Directory for state file | `./data` |
|
| `DNSWATCHER_DATA_DIR` | Directory for state file | `./data` |
|
||||||
| `DNSWATCHER_TARGETS` | Comma-separated list of DNS names to monitor | `""` |
|
| `DNSWATCHER_TARGETS` | Comma-separated DNS names (auto-classified via PSL) | `""` |
|
||||||
| `DNSWATCHER_SLACK_WEBHOOK` | Slack incoming webhook URL | `""` |
|
| `DNSWATCHER_SLACK_WEBHOOK` | Slack incoming webhook URL | `""` |
|
||||||
| `DNSWATCHER_MATTERMOST_WEBHOOK` | Mattermost incoming webhook URL | `""` |
|
| `DNSWATCHER_MATTERMOST_WEBHOOK` | Mattermost incoming webhook URL | `""` |
|
||||||
| `DNSWATCHER_NTFY_TOPIC` | ntfy topic URL | `""` |
|
| `DNSWATCHER_NTFY_TOPIC` | ntfy topic URL | `""` |
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -9,7 +9,6 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
go.uber.org/fx v1.24.0
|
go.uber.org/fx v1.24.0
|
||||||
golang.org/x/net v0.50.0
|
golang.org/x/net v0.50.0
|
||||||
)
|
)
|
||||||
@@ -17,12 +16,10 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
@@ -40,5 +37,4 @@ require (
|
|||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,59 +1,83 @@
|
|||||||
// Package config provides application configuration via Viper.
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClassifyTarget determines whether a DNS name is an apex domain
|
// DNSNameType indicates whether a DNS name is an apex domain or a hostname.
|
||||||
// (eTLD+1) or a hostname (subdomain of an eTLD+1). Returns
|
type DNSNameType int
|
||||||
// "domain" or "hostname". Returns an error if the name is itself
|
|
||||||
// a public suffix (e.g. "co.uk") or otherwise invalid.
|
const (
|
||||||
func ClassifyTarget(name string) (string, error) {
|
// DNSNameTypeDomain indicates the name is an apex (eTLD+1) domain.
|
||||||
// Normalize: lowercase, strip trailing dot.
|
DNSNameTypeDomain DNSNameType = iota
|
||||||
name = strings.ToLower(strings.TrimSuffix(name, "."))
|
// DNSNameTypeHostname indicates the name is a subdomain/hostname.
|
||||||
|
DNSNameTypeHostname
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrEmptyDNSName is returned when an empty string is passed to ClassifyDNSName.
|
||||||
|
var ErrEmptyDNSName = errors.New("empty DNS name")
|
||||||
|
|
||||||
|
// String returns the string representation of a DNSNameType.
|
||||||
|
func (t DNSNameType) String() string {
|
||||||
|
switch t {
|
||||||
|
case DNSNameTypeDomain:
|
||||||
|
return "domain"
|
||||||
|
case DNSNameTypeHostname:
|
||||||
|
return "hostname"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClassifyDNSName determines whether a DNS name is an apex domain or a
|
||||||
|
// hostname (subdomain) using the Public Suffix List. It returns an error
|
||||||
|
// if the input is empty or is itself a public suffix (e.g. "co.uk").
|
||||||
|
func ClassifyDNSName(name string) (DNSNameType, error) {
|
||||||
|
name = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(name), "."))
|
||||||
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return "", fmt.Errorf("empty target name")
|
return 0, ErrEmptyDNSName
|
||||||
}
|
}
|
||||||
|
|
||||||
apex, err := publicsuffix.EffectiveTLDPlusOne(name)
|
etld1, err := publicsuffix.EffectiveTLDPlusOne(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf(
|
return 0, fmt.Errorf("invalid DNS name %q: %w", name, err)
|
||||||
"invalid target %q: %w", name, err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == apex {
|
if name == etld1 {
|
||||||
return "domain", nil
|
return DNSNameTypeDomain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "hostname", nil
|
return DNSNameTypeHostname, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// classifyTargets splits a list of DNS names into apex domains
|
// ClassifyTargets splits a list of DNS names into apex domains and
|
||||||
// and hostnames using the Public Suffix List.
|
// hostnames using the Public Suffix List. It returns an error if any
|
||||||
func classifyTargets(
|
// name cannot be classified.
|
||||||
targets []string,
|
func ClassifyTargets(targets []string) ([]string, []string, error) {
|
||||||
) (domains, hostnames []string, err error) {
|
var domains, hostnames []string
|
||||||
for _, target := range targets {
|
|
||||||
kind, classifyErr := ClassifyTarget(target)
|
for _, t := range targets {
|
||||||
if classifyErr != nil {
|
normalized := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(t), "."))
|
||||||
return nil, nil, classifyErr
|
|
||||||
|
if normalized == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch kind {
|
typ, classErr := ClassifyDNSName(normalized)
|
||||||
case "domain":
|
if classErr != nil {
|
||||||
domains = append(domains, strings.ToLower(
|
return nil, nil, classErr
|
||||||
strings.TrimSuffix(target, "."),
|
}
|
||||||
))
|
|
||||||
case "hostname":
|
switch typ {
|
||||||
hostnames = append(hostnames, strings.ToLower(
|
case DNSNameTypeDomain:
|
||||||
strings.TrimSuffix(target, "."),
|
domains = append(domains, normalized)
|
||||||
))
|
case DNSNameTypeHostname:
|
||||||
|
hostnames = append(hostnames, normalized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,52 @@
|
|||||||
package config
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"sneak.berlin/go/dnswatcher/internal/config"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClassifyTarget(t *testing.T) {
|
func TestClassifyDNSName(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
expected string
|
want config.DNSNameType
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"apex .com", "example.com", "domain", false},
|
{name: "apex domain simple", input: "example.com", want: config.DNSNameTypeDomain},
|
||||||
{"apex .org", "example.org", "domain", false},
|
{name: "hostname simple", input: "www.example.com", want: config.DNSNameTypeHostname},
|
||||||
{"apex .co.uk", "example.co.uk", "domain", false},
|
{name: "apex domain multi-part TLD", input: "example.co.uk", want: config.DNSNameTypeDomain},
|
||||||
{"apex .com.au", "example.com.au", "domain", false},
|
{name: "hostname multi-part TLD", input: "api.example.co.uk", want: config.DNSNameTypeHostname},
|
||||||
{"subdomain www", "www.example.com", "hostname", false},
|
{name: "public suffix itself", input: "co.uk", wantErr: true},
|
||||||
{"subdomain api", "api.example.com", "hostname", false},
|
{name: "empty string", input: "", wantErr: true},
|
||||||
{"deep subdomain", "a.b.c.example.com", "hostname", false},
|
{name: "deeply nested hostname", input: "a.b.c.example.com", want: config.DNSNameTypeHostname},
|
||||||
{"subdomain .co.uk", "www.example.co.uk", "hostname", false},
|
{name: "trailing dot stripped", input: "example.com.", want: config.DNSNameTypeDomain},
|
||||||
{"trailing dot", "example.com.", "domain", false},
|
{name: "uppercase normalized", input: "WWW.Example.COM", want: config.DNSNameTypeHostname},
|
||||||
{"trailing dot sub", "www.example.com.", "hostname", false},
|
|
||||||
{"uppercase", "EXAMPLE.COM", "domain", false},
|
|
||||||
{"mixed case", "Www.Example.Com", "hostname", false},
|
|
||||||
{"public suffix", "co.uk", "", true},
|
|
||||||
{"tld only", "com", "", true},
|
|
||||||
{"empty", "", "", true},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
result, err := ClassifyTarget(tc.input)
|
got, err := config.ClassifyDNSName(tt.input)
|
||||||
if tc.wantErr {
|
|
||||||
assert.Error(t, err)
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ClassifyDNSName(%q) expected error, got %v", tt.input, got)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
if err != nil {
|
||||||
assert.Equal(t, tc.expected, result)
|
t.Fatalf("ClassifyDNSName(%q) unexpected error: %v", tt.input, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("ClassifyDNSName(%q) = %v, want %v", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,32 +54,30 @@ func TestClassifyTarget(t *testing.T) {
|
|||||||
func TestClassifyTargets(t *testing.T) {
|
func TestClassifyTargets(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
targets := []string{
|
domains, hostnames, err := config.ClassifyTargets([]string{
|
||||||
"example.com",
|
"example.com",
|
||||||
"www.example.com",
|
"www.example.com",
|
||||||
"api.example.com",
|
|
||||||
"example.co.uk",
|
"example.co.uk",
|
||||||
"blog.example.co.uk",
|
"api.example.co.uk",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
domains, hostnames, err := classifyTargets(targets)
|
if len(domains) != 2 {
|
||||||
require.NoError(t, err)
|
t.Errorf("expected 2 domains, got %d: %v", len(domains), domains)
|
||||||
|
|
||||||
assert.Equal(
|
|
||||||
t,
|
|
||||||
[]string{"example.com", "example.co.uk"},
|
|
||||||
domains,
|
|
||||||
)
|
|
||||||
assert.Equal(
|
|
||||||
t,
|
|
||||||
[]string{"www.example.com", "api.example.com", "blog.example.co.uk"},
|
|
||||||
hostnames,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClassifyTargets_RejectsPublicSuffix(t *testing.T) {
|
if len(hostnames) != 2 {
|
||||||
|
t.Errorf("expected 2 hostnames, got %d: %v", len(hostnames), hostnames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassifyTargetsRejectsPublicSuffix(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, _, err := classifyTargets([]string{"example.com", "co.uk"})
|
_, _, err := config.ClassifyTargets([]string{"co.uk"})
|
||||||
assert.Error(t, err)
|
if err == nil {
|
||||||
|
t.Error("expected error for public suffix, got nil")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,11 +132,11 @@ func buildConfig(
|
|||||||
tlsInterval = defaultTLSInterval
|
tlsInterval = defaultTLSInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
targets := parseCSV(viper.GetString("TARGETS"))
|
domains, hostnames, err := ClassifyTargets(
|
||||||
|
parseCSV(viper.GetString("TARGETS")),
|
||||||
domains, hostnames, classifyErr := classifyTargets(targets)
|
)
|
||||||
if classifyErr != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("classifying targets: %w", classifyErr)
|
return nil, fmt.Errorf("invalid targets configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
@@ -48,6 +49,9 @@ type Service struct {
|
|||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
client *http.Client
|
client *http.Client
|
||||||
config *config.Config
|
config *config.Config
|
||||||
|
ntfyURL *url.URL
|
||||||
|
slackWebhookURL *url.URL
|
||||||
|
mattermostWebhookURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new notify Service.
|
// New creates a new notify Service.
|
||||||
@@ -55,13 +59,44 @@ func New(
|
|||||||
_ fx.Lifecycle,
|
_ fx.Lifecycle,
|
||||||
params Params,
|
params Params,
|
||||||
) (*Service, error) {
|
) (*Service, error) {
|
||||||
return &Service{
|
svc := &Service{
|
||||||
log: params.Logger.Get(),
|
log: params.Logger.Get(),
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: httpClientTimeout,
|
Timeout: httpClientTimeout,
|
||||||
},
|
},
|
||||||
config: params.Config,
|
config: params.Config,
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
if params.Config.NtfyTopic != "" {
|
||||||
|
u, err := url.ParseRequestURI(params.Config.NtfyTopic)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ntfy topic URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.ntfyURL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Config.SlackWebhook != "" {
|
||||||
|
u, err := url.ParseRequestURI(params.Config.SlackWebhook)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid slack webhook URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.slackWebhookURL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Config.MattermostWebhook != "" {
|
||||||
|
u, err := url.ParseRequestURI(params.Config.MattermostWebhook)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"invalid mattermost webhook URL: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.mattermostWebhookURL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendNotification sends a notification to all configured endpoints.
|
// SendNotification sends a notification to all configured endpoints.
|
||||||
@@ -69,13 +104,13 @@ func (svc *Service) SendNotification(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
title, message, priority string,
|
title, message, priority string,
|
||||||
) {
|
) {
|
||||||
if svc.config.NtfyTopic != "" {
|
if svc.ntfyURL != nil {
|
||||||
go func() {
|
go func() {
|
||||||
notifyCtx := context.WithoutCancel(ctx)
|
notifyCtx := context.WithoutCancel(ctx)
|
||||||
|
|
||||||
err := svc.sendNtfy(
|
err := svc.sendNtfy(
|
||||||
notifyCtx,
|
notifyCtx,
|
||||||
svc.config.NtfyTopic,
|
svc.ntfyURL,
|
||||||
title, message, priority,
|
title, message, priority,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -87,13 +122,13 @@ func (svc *Service) SendNotification(
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if svc.config.SlackWebhook != "" {
|
if svc.slackWebhookURL != nil {
|
||||||
go func() {
|
go func() {
|
||||||
notifyCtx := context.WithoutCancel(ctx)
|
notifyCtx := context.WithoutCancel(ctx)
|
||||||
|
|
||||||
err := svc.sendSlack(
|
err := svc.sendSlack(
|
||||||
notifyCtx,
|
notifyCtx,
|
||||||
svc.config.SlackWebhook,
|
svc.slackWebhookURL,
|
||||||
title, message, priority,
|
title, message, priority,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -105,13 +140,13 @@ func (svc *Service) SendNotification(
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if svc.config.MattermostWebhook != "" {
|
if svc.mattermostWebhookURL != nil {
|
||||||
go func() {
|
go func() {
|
||||||
notifyCtx := context.WithoutCancel(ctx)
|
notifyCtx := context.WithoutCancel(ctx)
|
||||||
|
|
||||||
err := svc.sendSlack(
|
err := svc.sendSlack(
|
||||||
notifyCtx,
|
notifyCtx,
|
||||||
svc.config.MattermostWebhook,
|
svc.mattermostWebhookURL,
|
||||||
title, message, priority,
|
title, message, priority,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -126,18 +161,19 @@ func (svc *Service) SendNotification(
|
|||||||
|
|
||||||
func (svc *Service) sendNtfy(
|
func (svc *Service) sendNtfy(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
topic, title, message, priority string,
|
topicURL *url.URL,
|
||||||
|
title, message, priority string,
|
||||||
) error {
|
) error {
|
||||||
svc.log.Debug(
|
svc.log.Debug(
|
||||||
"sending ntfy notification",
|
"sending ntfy notification",
|
||||||
"topic", topic,
|
"topic", topicURL.String(),
|
||||||
"title", title,
|
"title", title,
|
||||||
)
|
)
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(
|
request, err := http.NewRequestWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
topic,
|
topicURL.String(),
|
||||||
bytes.NewBufferString(message),
|
bytes.NewBufferString(message),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,7 +183,7 @@ func (svc *Service) sendNtfy(
|
|||||||
request.Header.Set("Title", title)
|
request.Header.Set("Title", title)
|
||||||
request.Header.Set("Priority", ntfyPriority(priority))
|
request.Header.Set("Priority", ntfyPriority(priority))
|
||||||
|
|
||||||
resp, err := svc.client.Do(request)
|
resp, err := svc.client.Do(request) //nolint:gosec // URL validated at Service construction time
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("sending ntfy request: %w", err)
|
return fmt.Errorf("sending ntfy request: %w", err)
|
||||||
}
|
}
|
||||||
@@ -193,11 +229,12 @@ type SlackAttachment struct {
|
|||||||
|
|
||||||
func (svc *Service) sendSlack(
|
func (svc *Service) sendSlack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
webhookURL, title, message, priority string,
|
webhookURL *url.URL,
|
||||||
|
title, message, priority string,
|
||||||
) error {
|
) error {
|
||||||
svc.log.Debug(
|
svc.log.Debug(
|
||||||
"sending webhook notification",
|
"sending webhook notification",
|
||||||
"url", webhookURL,
|
"url", webhookURL.String(),
|
||||||
"title", title,
|
"title", title,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -219,7 +256,7 @@ func (svc *Service) sendSlack(
|
|||||||
request, err := http.NewRequestWithContext(
|
request, err := http.NewRequestWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
webhookURL,
|
webhookURL.String(),
|
||||||
bytes.NewBuffer(body),
|
bytes.NewBuffer(body),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -228,7 +265,7 @@ func (svc *Service) sendSlack(
|
|||||||
|
|
||||||
request.Header.Set("Content-Type", "application/json")
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := svc.client.Do(request)
|
resp, err := svc.client.Do(request) //nolint:gosec // URL validated at Service construction time
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("sending webhook request: %w", err)
|
return fmt.Errorf("sending webhook request: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user