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  }