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, &notification)
   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(&registerNotificationResponse{}).
   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  }