github.com/mweagle/Sparta@v1.15.0/execute_awsbinary.go (about) 1 // +build lambdabinary 2 3 package sparta 4 5 import ( 6 "context" 7 "encoding/json" 8 "fmt" 9 "os" 10 "reflect" 11 "sync" 12 13 awsLambdaGo "github.com/aws/aws-lambda-go/lambda" 14 awsLambdaContext "github.com/aws/aws-lambda-go/lambdacontext" 15 spartaAWS "github.com/mweagle/Sparta/aws" 16 cloudformationResources "github.com/mweagle/Sparta/aws/cloudformation/resources" 17 gocf "github.com/mweagle/go-cloudformation" 18 "github.com/sirupsen/logrus" 19 ) 20 21 // StampedServiceName is the name stamp 22 // https://blog.cloudflare.com/setting-go-variables-at-compile-time/ 23 // StampedServiceName is the serviceName stamped into this binary 24 var StampedServiceName string 25 26 // StampedBuildID is the buildID stamped into the binary 27 var StampedBuildID string 28 29 var discoveryInfo *DiscoveryInfo 30 var once sync.Once 31 32 func initDiscoveryInfo() { 33 info, _ := Discover() 34 discoveryInfo = info 35 } 36 37 func awsLambdaFunctionName(internalFunctionName string) gocf.Stringable { 38 // TODO - move this to use SSM so that it's not human editable? 39 // But discover information is per-function, not per stack. 40 // Could we put the stack discovery info in there? 41 once.Do(initDiscoveryInfo) 42 sanitizedName := awsLambdaInternalName(internalFunctionName) 43 44 return gocf.String(fmt.Sprintf("%s%s%s", 45 discoveryInfo.StackName, 46 functionNameDelimiter, 47 sanitizedName)) 48 } 49 50 func takesContext(handler reflect.Type) bool { 51 handlerTakesContext := false 52 if handler.NumIn() > 0 { 53 contextType := reflect.TypeOf((*context.Context)(nil)).Elem() 54 argumentType := handler.In(0) 55 handlerTakesContext = argumentType.Implements(contextType) 56 } 57 return handlerTakesContext 58 } 59 60 // tappedHandler is the handler that represents this binary's mode 61 func tappedHandler(handlerSymbol interface{}, 62 interceptors *LambdaEventInterceptors, 63 logger *logrus.Logger) interface{} { 64 65 // If there aren't any, make it a bit easier 66 // to call the applyInterceptors function 67 if interceptors == nil { 68 interceptors = &LambdaEventInterceptors{} 69 } 70 71 // Tap the call chain to inject the context params... 72 handler := reflect.ValueOf(handlerSymbol) 73 handlerType := reflect.TypeOf(handlerSymbol) 74 takesContext := takesContext(handlerType) 75 76 // Apply interceptors is a utility function to apply the 77 // specified interceptors as part of the lifecycle handler. 78 // We can push the specific behaviors into the interceptors 79 // and keep this function simple. 🎉 80 applyInterceptors := func(ctx context.Context, 81 msg json.RawMessage, 82 interceptors InterceptorList) context.Context { 83 for _, eachInterceptor := range interceptors { 84 ctx = eachInterceptor.Interceptor(ctx, msg) 85 } 86 return ctx 87 } 88 89 // How to determine if this handler has tracing enabled? That would be a property 90 // of the function template associated with this function. 91 92 // TODO - add Context.Timeout handler to ensure orderly exit 93 return func(ctx context.Context, msg json.RawMessage) (interface{}, error) { 94 95 awsSession := spartaAWS.NewSession(logger) 96 ctx = applyInterceptors(ctx, msg, interceptors.Begin) 97 ctx = context.WithValue(ctx, ContextKeyLogger, logger) 98 ctx = context.WithValue(ctx, ContextKeyAWSSession, awsSession) 99 ctx = applyInterceptors(ctx, msg, interceptors.BeforeSetup) 100 101 // Create the entry logger that has some context information 102 var logrusEntry *logrus.Entry 103 lambdaContext, lambdaContextOk := awsLambdaContext.FromContext(ctx) 104 if lambdaContextOk { 105 logrusEntry = logrus. 106 NewEntry(logger). 107 WithFields(logrus.Fields{ 108 LogFieldRequestID: lambdaContext.AwsRequestID, 109 LogFieldARN: lambdaContext.InvokedFunctionArn, 110 LogFieldBuildID: StampedBuildID, 111 LogFieldInstanceID: InstanceID(), 112 }) 113 } else { 114 logrusEntry = logrus.NewEntry(logger) 115 } 116 ctx = context.WithValue(ctx, ContextKeyRequestLogger, logrusEntry) 117 ctx = applyInterceptors(ctx, msg, interceptors.AfterSetup) 118 119 // construct arguments 120 var args []reflect.Value 121 if takesContext { 122 args = append(args, reflect.ValueOf(ctx)) 123 } 124 if (handlerType.NumIn() == 1 && !takesContext) || 125 handlerType.NumIn() == 2 { 126 eventType := handlerType.In(handlerType.NumIn() - 1) 127 event := reflect.New(eventType) 128 unmarshalErr := json.Unmarshal(msg, event.Interface()) 129 if unmarshalErr != nil { 130 return nil, unmarshalErr 131 } 132 args = append(args, event.Elem()) 133 } 134 ctx = applyInterceptors(ctx, msg, interceptors.BeforeDispatch) 135 response := handler.Call(args) 136 ctx = applyInterceptors(ctx, msg, interceptors.AfterDispatch) 137 138 // If the user function 139 // convert return values into (interface{}, error) 140 var err error 141 if len(response) > 0 { 142 if errVal, ok := response[len(response)-1].Interface().(error); ok { 143 err = errVal 144 } 145 } 146 ctx = context.WithValue(ctx, ContextKeyLambdaError, err) 147 var val interface{} 148 if len(response) > 1 { 149 val = response[0].Interface() 150 } 151 ctx = context.WithValue(ctx, ContextKeyLambdaResponse, val) 152 applyInterceptors(ctx, msg, interceptors.Complete) 153 return val, err 154 } 155 } 156 157 // Execute creates an HTTP listener to dispatch execution. Typically 158 // called via Main() via command line arguments. 159 func Execute(serviceName string, 160 lambdaAWSInfos []*LambdaAWSInfo, 161 logger *logrus.Logger) error { 162 163 logger.Debug("Initializing discovery service") 164 165 // Initialize the discovery service 166 initializeDiscovery(logger) 167 168 // Find the function name based on the dispatch 169 // https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html 170 requestedLambdaFunctionName := os.Getenv("AWS_LAMBDA_FUNCTION_NAME") 171 logger.WithField("lambdaName", requestedLambdaFunctionName). 172 Debug("Invoking requested lambda") 173 174 // Log any info when we start up... 175 logger.Debug("Querying for platform info") 176 platformLogSysInfo(requestedLambdaFunctionName, logger) 177 178 // So what if we have workflow hooks in here? 179 var interceptors *LambdaEventInterceptors 180 181 /* 182 There are three types of targets: 183 - User functions 184 - User custom resources 185 - Sparta custom resources 186 */ 187 // Based on the environment variable, setup the proper listener... 188 var lambdaFunctionName gocf.Stringable 189 testAWSName := "" 190 var handlerSymbol interface{} 191 knownNames := []string{} 192 193 ////////////////////////////////////////////////////////////////////////////// 194 // User registered commands? 195 ////////////////////////////////////////////////////////////////////////////// 196 logger.Debug("Checking user-defined lambda functions") 197 for _, eachLambdaInfo := range lambdaAWSInfos { 198 lambdaFunctionName = awsLambdaFunctionName(eachLambdaInfo.lambdaFunctionName()) 199 testAWSName = lambdaFunctionName.String().Literal 200 201 knownNames = append(knownNames, testAWSName) 202 if requestedLambdaFunctionName == testAWSName { 203 handlerSymbol = eachLambdaInfo.handlerSymbol 204 interceptors = eachLambdaInfo.Interceptors 205 206 } 207 208 // User defined custom resource handler? 209 for _, eachCustomResource := range eachLambdaInfo.customResources { 210 lambdaFunctionName = awsLambdaFunctionName(eachCustomResource.userFunctionName) 211 testAWSName = lambdaFunctionName.String().Literal 212 knownNames = append(knownNames, testAWSName) 213 if requestedLambdaFunctionName == testAWSName { 214 handlerSymbol = eachCustomResource.handlerSymbol 215 } 216 } 217 if handlerSymbol != nil { 218 break 219 } 220 } 221 222 ////////////////////////////////////////////////////////////////////////////// 223 // Request to instantiate a CustomResourceHandler that implements 224 // the CustomResourceCommand interface? 225 ////////////////////////////////////////////////////////////////////////////// 226 if handlerSymbol == nil { 227 logger.Debug("Checking CustomResourceHandler lambda functions") 228 229 requestCustomResourceType := os.Getenv(EnvVarCustomResourceTypeName) 230 if requestCustomResourceType != "" { 231 knownNames = append(knownNames, fmt.Sprintf("CloudFormation Custom Resource: %s", requestCustomResourceType)) 232 logger.WithFields(logrus.Fields{ 233 "customResourceTypeName": requestCustomResourceType, 234 }).Debug("Checking to see if there is a custom resource") 235 236 resource := gocf.NewResourceByType(requestCustomResourceType) 237 if resource != nil { 238 // Handler? 239 command, commandOk := resource.(cloudformationResources.CustomResourceCommand) 240 if !commandOk { 241 logger.Error("CloudFormation type %s doesn't implement cloudformationResources.CustomResourceCommand", requestCustomResourceType) 242 } else { 243 customHandler := cloudformationResources.CloudFormationLambdaCustomResourceHandler(command, logger) 244 if customHandler != nil { 245 handlerSymbol = customHandler 246 } 247 } 248 } else { 249 logger.Error("Failed to create CloudFormation custom resource of type: %s", requestCustomResourceType) 250 } 251 } 252 } 253 254 if handlerSymbol == nil { 255 errorMessage := fmt.Errorf("No handler found for AWS Lambda function: %s. Registered function name: %#v", 256 requestedLambdaFunctionName, 257 knownNames) 258 logger.Error(errorMessage) 259 return errorMessage 260 } 261 262 // Startup our version... 263 tappedHandler := tappedHandler(handlerSymbol, interceptors, logger) 264 awsLambdaGo.Start(tappedHandler) 265 return nil 266 }