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 }