github.com/mweagle/Sparta@v1.15.0/archetype/firehose.go (about)

     1  package archetype
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"runtime"
    13  	"strings"
    14  	"text/template"
    15  	"time"
    16  
    17  	"github.com/Masterminds/sprig"
    18  	awsEvents "github.com/aws/aws-lambda-go/events"
    19  	"github.com/aws/aws-sdk-go/aws/session"
    20  	sparta "github.com/mweagle/Sparta"
    21  	"github.com/mweagle/Sparta/archetype/xformer"
    22  	gocf "github.com/mweagle/go-cloudformation"
    23  	"github.com/pkg/errors"
    24  	"github.com/sirupsen/logrus"
    25  )
    26  
    27  func dropError() error {
    28  	return errors.New("KinesisFirehoseDrop")
    29  }
    30  
    31  // TemplateFileName is the name of the file in the ZIP archive
    32  const TemplateFileName = "xform.template"
    33  const xformResourcePrefix = "firehosexform_"
    34  const envVarKinesisFirehoseTransformName = "SPARTA_KINESIS_FIREHOSE_TRANSFORM"
    35  
    36  // KinesisFirehoseReactor represents a lambda function that responds to Dynamo  messages
    37  type KinesisFirehoseReactor interface {
    38  	// OnKinesisFirehoseRecord when an Kinesis reocrd occurs.
    39  	OnKinesisFirehoseRecord(ctx context.Context,
    40  		record *awsEvents.KinesisFirehoseEventRecord) (*awsEvents.KinesisFirehoseResponseRecord, error)
    41  }
    42  
    43  // KinesisFirehoseReactorFunc is a free function that adapts a KinesisFirehoseReactor
    44  // compliant signature into a function that exposes an OnEvent
    45  // function
    46  type KinesisFirehoseReactorFunc func(ctx context.Context,
    47  	kinesisRecord *awsEvents.KinesisFirehoseEventRecord) (*awsEvents.KinesisFirehoseResponseRecord, error)
    48  
    49  // OnKinesisFirehoseRecord satisfies the KinesisFirehoseReactor interface
    50  func (reactorFunc KinesisFirehoseReactorFunc) OnKinesisFirehoseRecord(ctx context.Context,
    51  	kinesisRecord *awsEvents.KinesisFirehoseEventRecord) (*awsEvents.KinesisFirehoseResponseRecord, error) {
    52  	return reactorFunc(ctx, kinesisRecord)
    53  }
    54  
    55  // ReactorName provides the name of the reactor func
    56  func (reactorFunc KinesisFirehoseReactorFunc) ReactorName() string {
    57  	return runtime.FuncForPC(reflect.ValueOf(reactorFunc).Pointer()).Name()
    58  }
    59  
    60  // NewKinesisFirehoseLambdaTransformer returns a new firehose proocessor that supports
    61  // transforming records.
    62  func NewKinesisFirehoseLambdaTransformer(reactor KinesisFirehoseReactor,
    63  	timeout time.Duration) (*sparta.LambdaAWSInfo, error) {
    64  
    65  	reactorLambda := func(ctx context.Context,
    66  		kinesisFirehoseEvent awsEvents.KinesisFirehoseEvent) (interface{}, error) {
    67  		// Apply the transform to each record and see
    68  		// what it says
    69  
    70  		response := &awsEvents.KinesisFirehoseResponse{
    71  			Records: make([]awsEvents.KinesisFirehoseResponseRecord,
    72  				len(kinesisFirehoseEvent.Records)),
    73  		}
    74  
    75  		var responseRecord *awsEvents.KinesisFirehoseResponseRecord
    76  		var responseRecordErr error
    77  		for eachIndex, eachRecord := range kinesisFirehoseEvent.Records {
    78  			responseRecord, responseRecordErr = reactor.OnKinesisFirehoseRecord(ctx, &eachRecord)
    79  			if responseRecordErr != nil {
    80  				return nil, errors.Wrapf(responseRecordErr, "Failed to transform record")
    81  			}
    82  			if responseRecord == nil {
    83  				responseRecord = &awsEvents.KinesisFirehoseResponseRecord{
    84  					RecordID: eachRecord.RecordID,
    85  					Result:   awsEvents.KinesisFirehoseTransformedStateDropped,
    86  					Data:     eachRecord.Data,
    87  				}
    88  			}
    89  			response.Records[eachIndex] = *responseRecord
    90  		}
    91  		return response, nil
    92  	}
    93  
    94  	lambdaFn, lambdaFnErr := sparta.NewAWSLambda(reactorName(reactor),
    95  		reactorLambda,
    96  		sparta.IAMRoleDefinition{})
    97  	return lambdaFn, lambdaFnErr
    98  }
    99  
   100  // NewKinesisFirehoseTransformer returns a new firehose proocessor that supports
   101  // transforming records.
   102  func NewKinesisFirehoseTransformer(xformFilePath string,
   103  	timeout time.Duration,
   104  	hooks *sparta.WorkflowHooks) (*sparta.LambdaAWSInfo, error) {
   105  
   106  	baseName := filepath.Base(xformFilePath)
   107  	archiveEntryName := sparta.CloudFormationResourceName(xformResourcePrefix, xformFilePath)
   108  	lambdaName := fmt.Sprintf("Firehose%s", baseName)
   109  
   110  	// Return a lambda function that applies the XForm transformation
   111  	reactorLambda := func(ctx context.Context,
   112  		kinesisEvent awsEvents.KinesisFirehoseEvent) (*awsEvents.KinesisFirehoseResponse, error) {
   113  		return lambdaXForm(ctx, kinesisEvent)
   114  	}
   115  	lambdaFn, lambdaFnErr := sparta.NewAWSLambda(lambdaName,
   116  		reactorLambda,
   117  		sparta.IAMRoleDefinition{})
   118  	if lambdaFnErr != nil {
   119  		return nil, errors.Wrapf(lambdaFnErr, "attempting to create Kinesis Firehose reactor")
   120  	}
   121  
   122  	// Borrow the resource name creator to get a name for the archive
   123  	lambdaFn.Options.Environment[envVarKinesisFirehoseTransformName] = gocf.String(archiveEntryName)
   124  	lambdaFn.Options.Timeout = (timeout.Milliseconds() / 1000)
   125  
   126  	// Create the decorator that adds the file to the ZIP archive using
   127  	// the transform name...
   128  	archiveDecorator := func(context map[string]interface{},
   129  		serviceName string,
   130  		zipWriter *zip.Writer,
   131  		awsSession *session.Session,
   132  		noop bool,
   133  		logger *logrus.Logger) error {
   134  		fileInfo, fileInfoErr := os.Stat(xformFilePath)
   135  		if fileInfoErr != nil {
   136  			return errors.Wrapf(fileInfoErr, "Failed to get fileInfo for Kinesis Firehose transform")
   137  		}
   138  		// G304: Potential file inclusion via variable
   139  		/* #nosec */
   140  		fileReader, fileReaderErr := os.Open(xformFilePath)
   141  		if fileReaderErr != nil {
   142  			return errors.Wrapf(fileReaderErr, "Failed to open Kinesis Firehose transform file")
   143  		}
   144  		defer func() {
   145  			closeErr := fileReader.Close()
   146  
   147  			if closeErr != nil {
   148  				logger.WithFields(logrus.Fields{
   149  					"error": closeErr,
   150  				}).Warn("Failed to close file reader")
   151  			}
   152  		}()
   153  
   154  		fileHeader, fileHeaderErr := zip.FileInfoHeader(fileInfo)
   155  		if fileHeaderErr != nil {
   156  			return errors.Wrapf(fileHeaderErr, "Failed to detect ZIP header for Kinesis Firehose transform")
   157  		}
   158  
   159  		fileHeader.Name = archiveEntryName
   160  		fileHeader.Method = zip.Deflate
   161  
   162  		// Copy it...
   163  		writer, writerErr := zipWriter.CreateHeader(fileHeader)
   164  		if writerErr != nil {
   165  			return errors.Wrapf(fileHeaderErr, "Failed to create ZIP header for Kinesis Firehose transform")
   166  		}
   167  		_, copyErr := io.Copy(writer, fileReader)
   168  		return copyErr
   169  	}
   170  	// Done...
   171  	hooks.Archives = append(hooks.Archives, sparta.ArchiveHookFunc(archiveDecorator))
   172  	return lambdaFn, nil
   173  }
   174  
   175  // ApplyTransformToKinesisFirehoseEvent is the generic transformation function that applies
   176  // a template.Template transformation to each
   177  func ApplyTransformToKinesisFirehoseEvent(ctx context.Context,
   178  	templateBytes []byte,
   179  	kinesisEvent awsEvents.KinesisFirehoseEvent) (*awsEvents.KinesisFirehoseResponse, error) {
   180  
   181  	logger, loggerOk := ctx.Value(sparta.ContextKeyLogger).(*logrus.Logger)
   182  	if loggerOk {
   183  		logger.Info("Hello world structured log message")
   184  	}
   185  
   186  	funcMap := sprig.TxtFuncMap()
   187  	funcMap["KinesisFirehoseDrop"] = interface{}(func() (string, error) {
   188  		return "", dropError()
   189  	})
   190  
   191  	// Setup the function map that knows how to do the JMESPath
   192  	// given the map...
   193  	transform, transformErr := template.
   194  		New("xformer").
   195  		Funcs(funcMap).
   196  		Parse(string(templateBytes))
   197  	if transformErr != nil {
   198  		return nil, errors.Wrapf(transformErr, "Attempting to create template")
   199  	}
   200  
   201  	response := &awsEvents.KinesisFirehoseResponse{
   202  		Records: make([]awsEvents.KinesisFirehoseResponseRecord, len(kinesisEvent.Records)),
   203  	}
   204  	headerInfo := &xformer.KinesisEventHeaderInfo{
   205  		InvocationID: kinesisEvent.InvocationID,
   206  
   207  		DeliveryStreamArn:      kinesisEvent.DeliveryStreamArn,
   208  		SourceKinesisStreamArn: kinesisEvent.SourceKinesisStreamArn,
   209  		Region:                 kinesisEvent.Region,
   210  	}
   211  
   212  	for eachIndex, eachRecord := range kinesisEvent.Records {
   213  		xformedRecord := awsEvents.KinesisFirehoseResponseRecord{
   214  			RecordID: eachRecord.RecordID,
   215  			Result:   awsEvents.KinesisFirehoseTransformedStateDropped,
   216  			Data:     eachRecord.Data,
   217  		}
   218  		xform, xformErr := xformer.NewKinesisFirehoseEventXFormer(headerInfo, &eachRecord)
   219  		if xformErr == nil {
   220  			dataMap := map[string]interface{}{
   221  				"Record": xform,
   222  			}
   223  			var outputBuffer bytes.Buffer
   224  			templateErr := transform.Execute(&outputBuffer, dataMap)
   225  			if templateErr != nil {
   226  				// Is the fail value "KinesisFirehoseDrop" ?
   227  				if !strings.Contains(templateErr.Error(), dropError().Error()) {
   228  					xformedRecord.Result = awsEvents.KinesisFirehoseTransformedStateProcessingFailed
   229  				}
   230  			} else if xform.Error() != nil {
   231  				xformedRecord.Result = awsEvents.KinesisFirehoseTransformedStateProcessingFailed
   232  			} else {
   233  				if loggerOk && logger.IsLevelEnabled(logrus.DebugLevel) {
   234  					logger.WithFields(logrus.Fields{
   235  						"input":  eachRecord.Data,
   236  						"output": outputBuffer.Bytes(),
   237  					}).Debug("Transformation result")
   238  				}
   239  
   240  				xformedRecord.Data = outputBuffer.Bytes()
   241  				xformedRecord.Result = awsEvents.KinesisFirehoseTransformedStateOk
   242  			}
   243  		}
   244  		// Save it...
   245  		response.Records[eachIndex] = xformedRecord
   246  	}
   247  	return response, nil
   248  }