github.com/argoproj/argo-events@v1.9.1/eventsources/sources/storagegrid/start.go (about) 1 /* 2 Copyright 2018 BlackRock, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package storagegrid 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "net/url" 26 "strings" 27 "time" 28 29 "github.com/argoproj/argo-events/pkg/apis/events" 30 "github.com/go-resty/resty/v2" 31 "github.com/google/uuid" 32 "github.com/joncalhoun/qson" 33 "go.uber.org/zap" 34 35 "github.com/argoproj/argo-events/common" 36 "github.com/argoproj/argo-events/common/logging" 37 eventsourcecommon "github.com/argoproj/argo-events/eventsources/common" 38 "github.com/argoproj/argo-events/eventsources/common/webhook" 39 "github.com/argoproj/argo-events/eventsources/sources" 40 apicommon "github.com/argoproj/argo-events/pkg/apis/common" 41 "github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1" 42 ) 43 44 // controller controls the webhook operations 45 var ( 46 controller = webhook.NewController() 47 ) 48 49 var ( 50 respBody = ` 51 <PublishResponse xmlns="http://argoevents-sns-server/"> 52 <PublishResult> 53 <MessageId>` + generateUUID().String() + `</MessageId> 54 </PublishResult> 55 <ResponseMetadata> 56 <RequestId>` + generateUUID().String() + `</RequestId> 57 </ResponseMetadata> 58 </PublishResponse>` + "\n" 59 60 notificationBodyTemplate = ` 61 <NotificationConfiguration> 62 <TopicConfiguration> 63 <Id>%s</Id> 64 <Topic>%s</Topic> 65 %s 66 </TopicConfiguration> 67 </NotificationConfiguration> 68 ` + "\n" 69 ) 70 71 // set up the activation and inactivation channels to control the state of routes. 72 func init() { 73 go webhook.ProcessRouteStatus(controller) 74 } 75 76 // generateUUID returns a new uuid 77 func generateUUID() uuid.UUID { 78 return uuid.New() 79 } 80 81 // filterName filters object key based on configured prefix and/or suffix 82 func filterName(notification *events.StorageGridNotification, eventSource *v1alpha1.StorageGridEventSource) bool { 83 if eventSource.Filter == nil { 84 return true 85 } 86 if eventSource.Filter.Prefix != "" && eventSource.Filter.Suffix != "" { 87 return strings.HasPrefix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Prefix) && strings.HasSuffix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Suffix) 88 } 89 if eventSource.Filter.Prefix != "" { 90 return strings.HasPrefix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Prefix) 91 } 92 if eventSource.Filter.Suffix != "" { 93 return strings.HasSuffix(notification.Message.Records[0].S3.Object.Key, eventSource.Filter.Suffix) 94 } 95 return true 96 } 97 98 // GetEventSourceName returns name of event source 99 func (el *EventListener) GetEventSourceName() string { 100 return el.EventSourceName 101 } 102 103 // GetEventName returns name of event 104 func (el *EventListener) GetEventName() string { 105 return el.EventName 106 } 107 108 // GetEventSourceType return type of event server 109 func (el *EventListener) GetEventSourceType() apicommon.EventSourceType { 110 return apicommon.StorageGridEvent 111 } 112 113 // Implement Router 114 // 1. GetRoute 115 // 2. HandleRoute 116 // 3. PostActivate 117 // 4. PostDeactivate 118 119 // GetRoute returns the route 120 func (router *Router) GetRoute() *webhook.Route { 121 return router.route 122 } 123 124 // HandleRoute handles new route 125 func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { 126 route := router.route 127 128 logger := route.Logger.With( 129 logging.LabelEndpoint, route.Context.Endpoint, 130 logging.LabelPort, route.Context.Port, 131 logging.LabelHTTPMethod, route.Context.Method, 132 ) 133 134 logger.Info("processing incoming request...") 135 136 if !route.Active { 137 logger.Warn("endpoint is inactive, won't process the request") 138 common.SendErrorResponse(writer, "inactive endpoint") 139 return 140 } 141 142 logger.Info("parsing the request body...") 143 request.Body = http.MaxBytesReader(writer, request.Body, route.Context.GetMaxPayloadSize()) 144 body, err := io.ReadAll(request.Body) 145 if err != nil { 146 logger.Errorw("failed to parse request body", zap.Error(err)) 147 common.SendErrorResponse(writer, "") 148 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 149 return 150 } 151 152 if request.Method == http.MethodHead { 153 respBody = "" 154 } 155 156 writer.WriteHeader(http.StatusOK) 157 writer.Header().Add("Content-Type", "text/plain") 158 if _, err := writer.Write([]byte(respBody)); err != nil { 159 logger.Errorw("failed to write the response", zap.Error(err)) 160 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 161 return 162 } 163 164 // notification received from storage grid is url encoded. 165 parsedURL, err := url.QueryUnescape(string(body)) 166 if err != nil { 167 logger.Errorw("failed to unescape request body url", zap.Error(err)) 168 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 169 return 170 } 171 b, err := qson.ToJSON(parsedURL) 172 if err != nil { 173 logger.Errorw("failed to convert request body in JSON format", zap.Error(err)) 174 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 175 return 176 } 177 178 logger.Info("converting request body to storage grid notification") 179 var notification *events.StorageGridNotification 180 err = json.Unmarshal(b, ¬ification) 181 if err != nil { 182 logger.Errorw("failed to convert the request body into storage grid notification", zap.Error(err)) 183 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 184 return 185 } 186 187 if filterName(notification, router.storageGridEventSource) { 188 defer func(start time.Time) { 189 route.Metrics.EventProcessingDuration(route.EventSourceName, route.EventName, float64(time.Since(start)/time.Millisecond)) 190 }(time.Now()) 191 192 logger.Info("new event received, dispatching event on route's data channel") 193 eventData := &events.StorageGridEventData{ 194 Notification: notification, 195 Metadata: router.storageGridEventSource.Metadata, 196 } 197 eventBody, err := json.Marshal(eventData) 198 if err != nil { 199 logger.Errorw("failed to marshal the event data", zap.Error(err)) 200 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 201 return 202 } 203 route.DataCh <- eventBody 204 return 205 } 206 207 logger.Warn("discarding notification since it did not pass all filters") 208 } 209 210 // PostActivate performs operations once the route is activated and ready to consume requests 211 func (router *Router) PostActivate() error { 212 eventSource := router.storageGridEventSource 213 route := router.route 214 215 authToken, err := common.GetSecretFromVolume(eventSource.AuthToken) 216 if err != nil { 217 return fmt.Errorf("AuthToken not found, %w", err) 218 } 219 220 registrationURL := common.FormattedURL(eventSource.Webhook.URL, eventSource.Webhook.Endpoint) 221 222 client := resty.New() 223 224 logger := route.Logger.With( 225 "registration-url", registrationURL, 226 "bucket", eventSource.Bucket, 227 "auth-secret-name", eventSource.AuthToken.Name, 228 "api-url", eventSource.APIURL, 229 ) 230 231 logger.Info("checking if the endpoint already exists...") 232 233 response, err := client.R(). 234 SetHeader("Content-Type", common.MediaTypeJSON). 235 SetAuthToken(authToken). 236 SetResult(&getEndpointResponse{}). 237 SetError(&genericResponse{}). 238 Get(common.FormattedURL(eventSource.APIURL, "/org/endpoints")) 239 if err != nil { 240 return err 241 } 242 243 if !response.IsSuccess() { 244 errObj := response.Error().(*genericResponse) 245 return fmt.Errorf("failed to list existing endpoints. reason: %s", errObj.Message.Text) 246 } 247 248 endpointResponse := response.Result().(*getEndpointResponse) 249 250 isURNExists := false 251 252 for _, endpoint := range endpointResponse.Data { 253 if endpoint.EndpointURN == eventSource.TopicArn { 254 logger.Info("endpoint with topic urn already exists, won't register duplicate endpoint") 255 isURNExists = true 256 break 257 } 258 } 259 260 if !isURNExists { 261 logger.Info("endpoint urn does not exist, registering a new endpoint") 262 newEndpoint := createEndpointRequest{ 263 DisplayName: router.route.EventName, 264 EndpointURI: common.FormattedURL(eventSource.Webhook.URL, eventSource.Webhook.Endpoint), 265 EndpointURN: eventSource.TopicArn, 266 AuthType: "anonymous", 267 InsecureTLS: true, 268 } 269 270 newEndpointBody, err := json.Marshal(&newEndpoint) 271 if err != nil { 272 return err 273 } 274 275 response, err := client.R(). 276 SetHeader("Content-Type", common.MediaTypeJSON). 277 SetAuthToken(authToken). 278 SetBody(string(newEndpointBody)). 279 SetResult(&genericResponse{}). 280 SetError(&genericResponse{}). 281 Post(common.FormattedURL(eventSource.APIURL, "/org/endpoints")) 282 if err != nil { 283 return err 284 } 285 286 if !response.IsSuccess() { 287 errObj := response.Error().(*genericResponse) 288 return fmt.Errorf("failed to register the endpoint. reason: %s", errObj.Message.Text) 289 } 290 291 logger.Info("successfully registered the endpoint") 292 } 293 294 logger.Info("registering notification configuration on storagegrid...") 295 296 var events []string 297 for _, event := range eventSource.Events { 298 events = append(events, fmt.Sprintf("<Event>%s</Event>", event)) 299 } 300 301 eventXML := strings.Join(events, "\n") 302 303 notificationBody := fmt.Sprintf(notificationBodyTemplate, route.EventName, eventSource.TopicArn, eventXML) 304 305 notification := &storageGridNotificationRequest{ 306 Notification: notificationBody, 307 } 308 309 notificationRequestBody, err := json.Marshal(notification) 310 if err != nil { 311 return err 312 } 313 314 response, err = client.R(). 315 SetHeader("Content-Type", common.MediaTypeJSON). 316 SetAuthToken(authToken). 317 SetBody(string(notificationRequestBody)). 318 SetResult(®isterNotificationResponse{}). 319 SetError(&genericResponse{}). 320 Put(common.FormattedURL(eventSource.APIURL, fmt.Sprintf("/org/containers/%s/notification", eventSource.Bucket))) 321 if err != nil { 322 return err 323 } 324 325 if !response.IsSuccess() { 326 errObj := response.Error().(*genericResponse) 327 return fmt.Errorf("failed to configure notification. reason %s", errObj.Message.Text) 328 } 329 330 logger.Info("successfully registered notification configuration on storagegrid") 331 return nil 332 } 333 334 // PostInactivate performs operations after the route is inactivated 335 func (router *Router) PostInactivate() error { 336 return nil 337 } 338 339 // StartListening starts an event source 340 func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error { 341 log := logging.FromContext(ctx). 342 With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName()) 343 log.Info("started processing the Storage Grid event source...") 344 defer sources.Recover(el.GetEventName()) 345 346 storagegridEventSource := &el.StorageGridEventSource 347 route := webhook.NewRoute(storagegridEventSource.Webhook, log, el.GetEventSourceName(), el.GetEventName(), el.Metrics) 348 349 return webhook.ManageRoute(ctx, &Router{ 350 route: route, 351 storageGridEventSource: storagegridEventSource, 352 }, controller, dispatch) 353 }