1
1
package datadog
2
2
3
3
import (
4
- "crypto/hmac"
5
- "crypto/sha256"
6
- "encoding/hex"
7
4
"encoding/json"
8
5
"errors"
9
6
"fmt"
10
- "github.com/brpaz/echozap"
11
7
payloadpkg "github.com/cirruslabs/cirrus-webhooks-server/internal/command/datadog/payload"
12
8
"github.com/cirruslabs/cirrus-webhooks-server/internal/datadogsender"
13
- mapset "github.com/deckarep/golang-set/v2 "
9
+ "github.com/cirruslabs/cirrus-webhooks-server/internal/server "
14
10
"github.com/labstack/echo/v4"
15
11
"github.com/spf13/cobra"
16
12
"go.uber.org/zap"
17
- "io"
18
- "net/http"
19
- "strings"
20
13
"time"
21
14
)
22
15
23
- var debug bool
24
- var httpAddr string
25
- var httpPath string
26
- var eventTypes []string
27
- var secretToken string
28
16
var dogstatsdAddr string
29
17
var apiKey string
30
18
var apiSite string
31
19
32
20
var (
33
- ErrDatadogFailed = errors .New ("failed to stream Cirrus CI events to Datadog" )
34
- ErrSignatureVerificationFailed = errors .New ("event signature verification failed" )
21
+ ErrDatadogFailed = errors .New ("failed to stream Cirrus CI events to Datadog" )
35
22
)
36
23
37
24
func NewCommand () * cobra.Command {
38
25
cmd := & cobra.Command {
39
26
Use : "datadog" ,
40
27
Short : "Stream Cirrus CI webhook events to Datadog" ,
41
- RunE : runDatadog ,
28
+ RunE : run ,
42
29
}
43
30
44
- cmd .PersistentFlags ().BoolVar (& debug , "debug" , false , "enable debug logging" )
45
- cmd .PersistentFlags ().StringVar (& httpAddr , "http-addr" , ":8080" ,
46
- "address on which the HTTP server will listen on" )
47
- cmd .PersistentFlags ().StringVar (& httpPath , "http-path" , "/" ,
48
- "HTTP path on which the webhook events will be expected" )
49
- cmd .PersistentFlags ().StringSliceVar (& eventTypes , "event-types" , []string {},
50
- "comma-separated list of the event types to limit processing to " +
51
- "(for example, --event-types=audit_event or --event-types=build,task" )
52
- cmd .PersistentFlags ().StringVar (& secretToken , "secret-token" , "" ,
53
- "if specified, this value will be used as a HMAC SHA-256 secret to verify the webhook events" )
31
+ server .AppendFlags (cmd )
32
+
54
33
cmd .PersistentFlags ().StringVar (& dogstatsdAddr , "dogstatsd-addr" , "" ,
55
34
"enables sending webhook events as Datadog events via the DogStatsD protocol to the specified address " +
56
35
"(for example, --dogstatsd-addr=127.0.0.1:8125)" )
57
36
cmd .PersistentFlags ().StringVar (& apiKey , "api-key" , "" ,
58
- "Enables sending webhook events as Datadog logs via the Datadog API using the specified API key" )
37
+ "enables sending webhook events as Datadog logs via the Datadog API using the specified API key" )
59
38
cmd .PersistentFlags ().StringVar (& apiSite , "api-site" , "datadoghq.com" ,
60
39
"specifies the Datadog site to use when sending webhook events as Datadog logs via the Datadog API" )
61
40
62
41
return cmd
63
42
}
64
43
65
- func runDatadog (cmd * cobra.Command , args []string ) error {
66
- // Initialize the logger
67
- config := zap .NewProductionConfig ()
68
- if debug {
69
- config .Level = zap .NewAtomicLevelAt (zap .DebugLevel )
70
- }
71
- logger := zap .Must (config .Build ()).Sugar ()
72
-
44
+ func run (cmd * cobra.Command , _ []string ) error {
73
45
// Initialize a Datadog sender
74
46
var sender datadogsender.Sender
75
47
var err error
@@ -88,99 +60,43 @@ func runDatadog(cmd *cobra.Command, args []string) error {
88
60
return err
89
61
}
90
62
91
- // Convert event types to a set for faster lookup
92
- eventTypesSet := mapset .NewSet [string ](eventTypes ... )
93
-
94
- // Configure HTTP server
95
- e := echo .New ()
96
-
97
- e .Use (echozap .ZapLogger (logger .Desugar ()))
98
-
99
- e .POST (httpPath , func (ctx echo.Context ) error {
100
- return processWebhookEvent (ctx , logger , sender , eventTypesSet )
101
- })
102
-
103
- server := & http.Server {
104
- Addr : httpAddr ,
105
- Handler : e ,
106
- ReadHeaderTimeout : 10 * time .Second ,
107
- }
108
-
109
- logger .Infof ("starting HTTP server on %s" , httpAddr )
110
-
111
- httpServerErrCh := make (chan error , 1 )
112
-
113
- go func () {
114
- httpServerErrCh <- server .ListenAndServe ()
115
- }()
116
-
117
- select {
118
- case <- cmd .Context ().Done ():
119
- if err := server .Close (); err != nil {
120
- return err
121
- }
122
- case httpServerErr := <- httpServerErrCh :
123
- return httpServerErr
124
- }
125
-
126
- return <- httpServerErrCh
63
+ return server .New (func (ctx echo.Context , presentedEventType string , body []byte , logger * zap.SugaredLogger ) error {
64
+ return processWebhookEvent (ctx , presentedEventType , body , sender , logger )
65
+ }, zap .S ()).Run (cmd .Context ())
127
66
}
128
67
129
68
func processWebhookEvent (
130
69
ctx echo.Context ,
131
- logger * zap.SugaredLogger ,
70
+ presentedEventType string ,
71
+ body []byte ,
132
72
sender datadogsender.Sender ,
133
- eventTypesSet mapset. Set [ string ] ,
73
+ logger * zap. SugaredLogger ,
134
74
) error {
135
- // Make sure this is an event we're looking for
136
- presentedEventType := ctx .Request ().Header .Get ("X-Cirrus-Event" )
137
-
138
- if eventTypesSet .Cardinality () != 0 && ! eventTypesSet .Contains (presentedEventType ) {
139
- logger .Debugf ("skipping event of type %q because we only process events of types %s" ,
140
- presentedEventType , strings .Join (eventTypesSet .ToSlice (), ", " ))
141
-
142
- return ctx .String (http .StatusOK , fmt .Sprintf ("skipping event of type %q" , presentedEventType ))
143
- }
144
-
145
- body , err := io .ReadAll (ctx .Request ().Body )
146
- if err != nil {
147
- logger .Warnf ("failed to read request's body: %v" , err )
75
+ // Decode the event
76
+ var payload payloadpkg.Payload
148
77
149
- return ctx .NoContent (http .StatusBadRequest )
78
+ switch presentedEventType {
79
+ case "audit_event" :
80
+ payload = & payloadpkg.AuditEvent {}
81
+ case "build" , "task" :
82
+ payload = & payloadpkg.BuildOrTask {}
83
+ default :
84
+ return nil
150
85
}
151
86
152
- // Verify that this event comes from the Cirrus CI
153
- if err := verifyEvent (ctx , body ); err != nil {
154
- logger .Warnf ("%v" , err )
155
-
156
- return ctx .NoContent (http .StatusBadRequest )
87
+ if err := json .Unmarshal (body , payload ); err != nil {
88
+ return fmt .Errorf ("failed to enrich Datadog event with tags: " +
89
+ "failed to parse the webhook event of type %q as JSON: %v" , presentedEventType , err )
157
90
}
158
91
159
- // Log this event into the Datadog
92
+ // Create a new Datadog event and enrich it with tags
160
93
evt := & datadogsender.Event {
161
94
Title : "Webhook event" ,
162
95
Text : string (body ),
163
96
Tags : []string {fmt .Sprintf ("webhook_event_type:%s" , presentedEventType )},
164
97
}
165
98
166
- // Enrich the event with tags
167
- var payload payloadpkg.Payload
168
-
169
- switch presentedEventType {
170
- case "audit_event" :
171
- payload = & payloadpkg.AuditEvent {}
172
- case "build" , "task" :
173
- payload = & payloadpkg.BuildOrTask {}
174
- }
175
-
176
- if payload != nil {
177
- if err = json .Unmarshal (body , payload ); err != nil {
178
- logger .Warnf ("failed to enrich Datadog event with tags: " +
179
- "failed to parse the webhook event of type %q as JSON: %v" , presentedEventType , err )
180
- } else {
181
- payload .Enrich (ctx .Request ().Header , evt , logger )
182
- }
183
- }
99
+ payload .Enrich (ctx .Request ().Header , evt , logger )
184
100
185
101
// Datadog silently discards log events submitted with a
186
102
// timestamp that is more than 18 hours in the past, sigh.
@@ -191,37 +107,10 @@ func processWebhookEvent(
191
107
"18 hours in the past, it'll likely going to be discarded" , presentedEventType )
192
108
}
193
109
194
- message , err := sender . SendEvent ( ctx . Request (). Context (), evt )
195
- if err != nil {
110
+ // Log this event to Datadog
111
+ if err := sender . SendEvent ( ctx . Request (). Context (), evt ); err != nil {
196
112
return fmt .Errorf ("%w: %v" , ErrDatadogFailed , err )
197
113
}
198
114
199
- return ctx .String (http .StatusCreated , message )
200
- }
201
-
202
- func verifyEvent (ctx echo.Context , body []byte ) error {
203
- // Nothing to do
204
- if secretToken == "" {
205
- return nil
206
- }
207
-
208
- // Calculate the expected signature
209
- hmacSHA256 := hmac .New (sha256 .New , []byte (secretToken ))
210
- hmacSHA256 .Write (body )
211
- expectedSignature := hmacSHA256 .Sum (nil )
212
-
213
- // Prepare the presented signature
214
- presentedSignatureRaw := ctx .Request ().Header .Get ("X-Cirrus-Signature" )
215
- presentedSignature , err := hex .DecodeString (presentedSignatureRaw )
216
- if err != nil {
217
- return fmt .Errorf ("%w: failed to hex-decode the signature %q: %v" ,
218
- ErrSignatureVerificationFailed , presentedSignatureRaw , err )
219
- }
220
-
221
- // Compare signatures
222
- if ! hmac .Equal (expectedSignature , presentedSignature ) {
223
- return fmt .Errorf ("%w: signature is not valid" , ErrSignatureVerificationFailed )
224
- }
225
-
226
115
return nil
227
116
}
0 commit comments