github.com/mweagle/Sparta@v1.15.0/sparta_main.go (about) 1 package sparta 2 3 import ( 4 "bytes" 5 cryptoRand "crypto/rand" 6 "crypto/sha1" 7 "encoding/hex" 8 "fmt" 9 "math/rand" 10 "os" 11 "os/exec" 12 "path" 13 "runtime" 14 "strings" 15 "time" 16 17 "github.com/sirupsen/logrus" 18 "github.com/spf13/cobra" 19 validator "gopkg.in/go-playground/validator.v9" 20 ) 21 22 // Constant for Sparta color aware stdout logging 23 const ( 24 redCode = 31 25 ) 26 27 // The Lambda instance ID for this execution 28 var instanceID string 29 30 // Validation instance 31 var validate *validator.Validate 32 33 func isRunningInAWS() bool { 34 return len(os.Getenv("AWS_LAMBDA_FUNCTION_NAME")) != 0 35 } 36 37 func displayPrettyHeader(headerDivider string, disableColors bool, logger *logrus.Logger) { 38 logger.Info(headerDivider) 39 red := func(inputText string) string { 40 if disableColors { 41 return inputText 42 } 43 return fmt.Sprintf("\x1b[%dm%s\x1b[0m", redCode, inputText) 44 } 45 logger.Info(fmt.Sprintf(red("╔═╗╔═╗╔═╗╦═╗╔╦╗╔═╗")+" Version : %s", SpartaVersion)) 46 logger.Info(fmt.Sprintf(red("╚═╗╠═╝╠═╣╠╦╝ ║ ╠═╣")+" SHA : %s", SpartaGitHash[0:7])) 47 logger.Info(fmt.Sprintf(red("╚═╝╩ ╩ ╩╩╚═ ╩ ╩ ╩")+" Go : %s", runtime.Version())) 48 logger.Info(headerDivider) 49 } 50 51 var codePipelineEnvironments map[string]map[string]string 52 53 func init() { 54 validate = validator.New() 55 codePipelineEnvironments = make(map[string]map[string]string) 56 57 r := rand.New(rand.NewSource(time.Now().UnixNano())) 58 instanceID = fmt.Sprintf("i-%d", r.Int63()) 59 } 60 61 // Logger returns the sparta Logger instance for this process 62 func Logger() *logrus.Logger { 63 return OptionsGlobal.Logger 64 } 65 66 // InstanceID returns the uniquely assigned instanceID for this lambda 67 // container 68 func InstanceID() string { 69 return instanceID 70 } 71 72 // CommandLineOptions defines the commands available via the Sparta command 73 // line interface. Embedding applications can extend existing commands 74 // and add their own to the `Root` command. See https://github.com/spf13/cobra 75 // for more information. 76 var CommandLineOptions = struct { 77 Root *cobra.Command 78 Version *cobra.Command 79 Provision *cobra.Command 80 Delete *cobra.Command 81 Execute *cobra.Command 82 Describe *cobra.Command 83 Explore *cobra.Command 84 Profile *cobra.Command 85 Status *cobra.Command 86 }{} 87 88 /*============================================================================*/ 89 // Provision options 90 // Ref: http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html 91 type optionsProvisionStruct struct { 92 S3Bucket string `validate:"required"` 93 BuildID string `validate:"-"` // non-whitespace 94 PipelineTrigger string `validate:"-"` 95 InPlace bool `validate:"-"` 96 } 97 98 var optionsProvision optionsProvisionStruct 99 100 func provisionBuildID(userSuppliedValue string, logger *logrus.Logger) (string, error) { 101 buildID := userSuppliedValue 102 if buildID == "" { 103 // That's cool, let's see if we can find a git SHA 104 cmd := exec.Command("git", 105 "rev-parse", 106 "HEAD") 107 var stdout bytes.Buffer 108 var stderr bytes.Buffer 109 cmd.Stdout = &stdout 110 cmd.Stderr = &stderr 111 cmdErr := cmd.Run() 112 if cmdErr == nil { 113 // Great, let's use the SHA 114 buildID = strings.TrimSpace(string(stdout.String())) 115 if buildID != "" { 116 logger.WithField("SHA", buildID). 117 WithField("Command", "git rev-parse HEAD"). 118 Info("Using `git` SHA for StampedBuildID") 119 } 120 } 121 // Ignore any errors and make up a random one 122 if buildID == "" { 123 // No problem, let's use an arbitrary SHA 124 hash := sha1.New() 125 randomBytes := make([]byte, 256) 126 _, err := cryptoRand.Read(randomBytes) 127 if err != nil { 128 return "", err 129 } 130 _, err = hash.Write(randomBytes) 131 if err != nil { 132 return "", err 133 } 134 buildID = hex.EncodeToString(hash.Sum(nil)) 135 } 136 } 137 return buildID, nil 138 } 139 140 /*============================================================================*/ 141 // Describe options 142 type optionsDescribeStruct struct { 143 OutputFile string `validate:"required"` 144 S3Bucket string `validate:"required"` 145 } 146 147 var optionsDescribe optionsDescribeStruct 148 149 /*============================================================================*/ 150 // Explore options? 151 type optionsExploreStruct struct { 152 InputExtensions []string `validate:"-"` 153 } 154 155 var optionsExplore optionsExploreStruct 156 157 /*============================================================================*/ 158 // Profile options 159 type optionsProfileStruct struct { 160 S3Bucket string `validate:"required"` 161 Port int `validate:"-"` 162 } 163 164 var optionsProfile optionsProfileStruct 165 166 /*============================================================================*/ 167 // Status options 168 type optionsStatusStruct struct { 169 Redact bool `validate:"-"` 170 } 171 172 var optionsStatus optionsStatusStruct 173 174 /*============================================================================*/ 175 // Initialization 176 // Initialize all the Cobra commands and their associated flags 177 /*============================================================================*/ 178 func init() { 179 // Root 180 CommandLineOptions.Root = &cobra.Command{ 181 Use: path.Base(os.Args[0]), 182 Short: "Sparta-powered AWS Lambda microservice", 183 SilenceErrors: true, 184 } 185 CommandLineOptions.Root.PersistentFlags().BoolVarP(&OptionsGlobal.Noop, "noop", 186 "n", 187 false, 188 "Dry-run behavior only (do not perform mutations)") 189 CommandLineOptions.Root.PersistentFlags().StringVarP(&OptionsGlobal.LogLevel, 190 "level", 191 "l", 192 "info", 193 "Log level [panic, fatal, error, warn, info, debug]") 194 CommandLineOptions.Root.PersistentFlags().StringVarP(&OptionsGlobal.LogFormat, 195 "format", 196 "f", 197 "text", 198 "Log format [text, json]") 199 CommandLineOptions.Root.PersistentFlags().BoolVarP(&OptionsGlobal.TimeStamps, 200 "timestamps", 201 "z", 202 false, 203 "Include UTC timestamp log line prefix") 204 CommandLineOptions.Root.PersistentFlags().StringVarP(&OptionsGlobal.BuildTags, 205 "tags", 206 "t", 207 "", 208 "Optional build tags for conditional compilation") 209 // Make sure there's a place to put any linker flags 210 CommandLineOptions.Root.PersistentFlags().StringVar(&OptionsGlobal.LinkerFlags, 211 "ldflags", 212 "", 213 "Go linker string definition flags (https://golang.org/cmd/link/)") 214 215 // Support disabling log colors for CLI friendliness 216 CommandLineOptions.Root.PersistentFlags().BoolVarP(&OptionsGlobal.DisableColors, 217 "nocolor", 218 "", 219 false, 220 "Boolean flag to suppress colorized TTY output") 221 222 // Version 223 CommandLineOptions.Version = &cobra.Command{ 224 Use: "version", 225 Short: "Display version information", 226 Long: `Displays the Sparta framework version `, 227 SilenceUsage: true, 228 Run: func(cmd *cobra.Command, args []string) { 229 230 }, 231 } 232 // Provision 233 CommandLineOptions.Provision = &cobra.Command{ 234 Use: "provision", 235 Short: "Provision service", 236 Long: `Provision the service (either create or update) via CloudFormation`, 237 SilenceUsage: true, 238 } 239 CommandLineOptions.Provision.Flags().StringVarP(&optionsProvision.S3Bucket, 240 "s3Bucket", 241 "s", 242 "", 243 "S3 Bucket to use for Lambda source") 244 CommandLineOptions.Provision.Flags().StringVarP(&optionsProvision.BuildID, 245 "buildID", 246 "i", 247 "", 248 "Optional BuildID to use") 249 CommandLineOptions.Provision.Flags().StringVarP(&optionsProvision.PipelineTrigger, 250 "codePipelinePackage", 251 "p", 252 "", 253 "Name of CodePipeline package that includes cloudformation.json Template and ZIP config files") 254 CommandLineOptions.Provision.Flags().BoolVarP(&optionsProvision.InPlace, 255 "inplace", 256 "c", 257 false, 258 "If the provision operation results in *only* function updates, bypass CloudFormation") 259 260 // Delete 261 CommandLineOptions.Delete = &cobra.Command{ 262 Use: "delete", 263 Short: "Delete service", 264 Long: `Ensure service is successfully deleted`, 265 SilenceUsage: true, 266 } 267 268 // Execute 269 CommandLineOptions.Execute = &cobra.Command{ 270 Use: "execute", 271 Short: "Start the application and begin handling events", 272 Long: `Start the application and begin handling events`, 273 SilenceUsage: true, 274 } 275 276 // Describe 277 CommandLineOptions.Describe = &cobra.Command{ 278 Use: "describe", 279 Short: "Describe service", 280 Long: `Produce an HTML report of the service`, 281 SilenceUsage: true, 282 } 283 CommandLineOptions.Describe.Flags().StringVarP(&optionsDescribe.OutputFile, 284 "out", 285 "o", 286 "", 287 "Output file for HTML description") 288 CommandLineOptions.Describe.Flags().StringVarP(&optionsDescribe.S3Bucket, 289 "s3Bucket", 290 "s", 291 "", 292 "S3 Bucket to use for Lambda source") 293 294 // Explore 295 CommandLineOptions.Explore = &cobra.Command{ 296 Use: "explore", 297 Short: "Interactively explore a provisioned service", 298 Long: `Startup a local CLI GUI to explore and trigger your AWS service`, 299 SilenceUsage: true, 300 } 301 CommandLineOptions.Explore.Flags().StringArrayVarP(&optionsExplore.InputExtensions, 302 "inputExtension", 303 "e", 304 []string{}, 305 "One or more file extensions to include as sample inputs") 306 307 // Profile 308 CommandLineOptions.Profile = &cobra.Command{ 309 Use: "profile", 310 Short: "Interactively examine service pprof output", 311 Long: `Startup a local pprof webserver to interrogate profiles snapshots on S3`, 312 SilenceUsage: true, 313 } 314 CommandLineOptions.Profile.Flags().StringVarP(&optionsProfile.S3Bucket, 315 "s3Bucket", 316 "s", 317 "", 318 "S3 Bucket that stores lambda profile snapshots") 319 CommandLineOptions.Profile.Flags().IntVarP(&optionsProfile.Port, 320 "port", 321 "p", 322 8080, 323 "Alternative port for `pprof` web UI (default=8080)") 324 325 // Status 326 CommandLineOptions.Status = &cobra.Command{ 327 Use: "status", 328 Short: "Produce a report for a provisioned service", 329 Long: `Produce a report for a provisioned service`, 330 SilenceUsage: true, 331 } 332 CommandLineOptions.Status.Flags().BoolVarP(&optionsStatus.Redact, "redact", 333 "r", 334 false, 335 "Redact AWS Account ID from report") 336 } 337 338 // CommandLineOptionsHook allows embedding applications the ability 339 // to validate caller-defined command line arguments. Return an error 340 // if the command line fails. 341 type CommandLineOptionsHook func(command *cobra.Command) error 342 343 // ParseOptions parses the command line options 344 func ParseOptions(handler CommandLineOptionsHook) error { 345 // First up, create a dummy Root command for the parse... 346 var parseCmdRoot = &cobra.Command{ 347 Use: CommandLineOptions.Root.Use, 348 Short: CommandLineOptions.Root.Short, 349 SilenceUsage: true, 350 SilenceErrors: false, 351 RunE: func(cmd *cobra.Command, args []string) error { 352 return nil 353 }, 354 } 355 parseCmdRoot.PersistentFlags().BoolVarP(&OptionsGlobal.Noop, "noop", 356 "n", 357 false, 358 "Dry-run behavior only (do not perform mutations)") 359 parseCmdRoot.PersistentFlags().StringVarP(&OptionsGlobal.LogLevel, 360 "level", 361 "l", 362 "info", 363 "Log level [panic, fatal, error, warn, info, debug]") 364 parseCmdRoot.PersistentFlags().StringVarP(&OptionsGlobal.LogFormat, 365 "format", 366 "f", 367 "text", 368 "Log format [text, json]") 369 parseCmdRoot.PersistentFlags().StringVarP(&OptionsGlobal.BuildTags, 370 "tags", 371 "t", 372 "", 373 "Optional build tags for conditional compilation") 374 375 // Now, for any user-attached commands, add them to the temporary Parse 376 // root command. 377 for _, eachUserCommand := range CommandLineOptions.Root.Commands() { 378 userProxyCmd := &cobra.Command{ 379 Use: eachUserCommand.Use, 380 Short: eachUserCommand.Short, 381 } 382 userProxyCmd.PreRunE = func(cmd *cobra.Command, args []string) error { 383 validateErr := validate.Struct(OptionsGlobal) 384 if nil != validateErr { 385 return validateErr 386 } 387 // Format? 388 var formatter logrus.Formatter 389 switch OptionsGlobal.LogFormat { 390 case "text", "txt": 391 formatter = &logrus.TextFormatter{} 392 case "json": 393 formatter = &logrus.JSONFormatter{} 394 } 395 logger, loggerErr := NewLoggerWithFormatter(OptionsGlobal.LogLevel, formatter) 396 if nil != loggerErr { 397 return loggerErr 398 } 399 OptionsGlobal.Logger = logger 400 401 if handler != nil { 402 return handler(userProxyCmd) 403 } 404 return nil 405 } 406 userProxyCmd.Flags().AddFlagSet(eachUserCommand.Flags()) 407 parseCmdRoot.AddCommand(userProxyCmd) 408 } 409 410 ////////////////////////////////////////////////////////////////////////////// 411 // Then add the standard Sparta ones... 412 spartaCommands := []*cobra.Command{ 413 CommandLineOptions.Version, 414 CommandLineOptions.Provision, 415 CommandLineOptions.Delete, 416 CommandLineOptions.Execute, 417 CommandLineOptions.Describe, 418 CommandLineOptions.Explore, 419 CommandLineOptions.Profile, 420 CommandLineOptions.Status, 421 } 422 for _, eachCommand := range spartaCommands { 423 eachCommand.PreRunE = func(cmd *cobra.Command, args []string) error { 424 if eachCommand == CommandLineOptions.Provision { 425 StampedBuildID = optionsProvision.BuildID 426 } 427 if handler != nil { 428 return handler(eachCommand) 429 } 430 return nil 431 } 432 parseCmdRoot.AddCommand(CommandLineOptions.Version) 433 } 434 435 // Assign each command an empty RunE func s.t. 436 // Cobra doesn't print out the command info 437 for _, eachCommand := range parseCmdRoot.Commands() { 438 eachCommand.RunE = func(cmd *cobra.Command, args []string) error { 439 return nil 440 } 441 } 442 // Intercept the usage command - we'll end up showing this later 443 // in Main...If there is an error, we will show help there... 444 parseCmdRoot.SetHelpFunc(func(*cobra.Command, []string) { 445 // Swallow help here 446 }) 447 448 // Run it... 449 executeErr := parseCmdRoot.Execute() 450 451 // Cleanup the Sparta specific ones 452 for _, eachCmd := range spartaCommands { 453 eachCmd.RunE = nil 454 eachCmd.PreRunE = nil 455 } 456 457 if nil != executeErr { 458 parseCmdRoot.SetHelpFunc(nil) 459 executeErr = parseCmdRoot.Root().Help() 460 } 461 return executeErr 462 } 463 464 // NewLogger returns a new logrus.Logger instance. It is the caller's responsibility 465 // to set the formatter if needed. 466 func NewLogger(level string) (*logrus.Logger, error) { 467 return NewLoggerWithFormatter(level, nil) 468 }