github.com/kristofferahl/go-centry@v1.5.0/cmd/centry/runtime.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/kristofferahl/go-centry/internal/pkg/config"
     8  	"github.com/kristofferahl/go-centry/internal/pkg/log"
     9  	"github.com/sirupsen/logrus"
    10  	"github.com/urfave/cli/v2"
    11  )
    12  
    13  const metadataExitCode string = "exitcode"
    14  
    15  // Runtime defines the runtime
    16  type Runtime struct {
    17  	cli     *cli.App
    18  	context *Context
    19  	file    string
    20  	args    []string
    21  	events  []string
    22  }
    23  
    24  // NewRuntime builds a runtime based on the given arguments
    25  func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) {
    26  	// Create the runtime
    27  	runtime := &Runtime{
    28  		cli:     nil,
    29  		context: context,
    30  		file:    "./centry.yaml",
    31  		args:    []string{},
    32  		events:  []string{},
    33  	}
    34  
    35  	// Env manifest file
    36  	err := initFromEnvironment(runtime)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	// Args and manifest file
    42  	err = initFromArgs(runtime, inputArgs)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	// Load manifest
    48  	manifest, err := config.LoadManifest(runtime.file)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	context.manifest = manifest
    53  
    54  	// Create the log manager
    55  	context.log = log.CreateManager(context.manifest.Config.Log.Level, context.manifest.Config.Log.Prefix, context.io)
    56  
    57  	// Create global options
    58  	options := createGlobalOptions(runtime)
    59  
    60  	// Configure default options
    61  	configureDefaultOptions()
    62  
    63  	// Initialize cli
    64  	runtime.cli = &cli.App{
    65  		Name:      context.manifest.Config.Name,
    66  		HelpName:  context.manifest.Config.Name,
    67  		Usage:     context.manifest.Config.Description,
    68  		UsageText: "",
    69  		Version:   context.manifest.Config.Version,
    70  
    71  		Commands: make([]*cli.Command, 0),
    72  		Flags:    optionsSetToFlags(options),
    73  
    74  		HideHelpCommand:       true,
    75  		CustomAppHelpTemplate: cliHelpTemplate,
    76  		EnableBashCompletion:  true,
    77  
    78  		Writer:    context.io.Stdout,
    79  		ErrWriter: context.io.Stderr,
    80  
    81  		Before: func(c *cli.Context) error {
    82  			return handleBefore(runtime, c)
    83  		},
    84  		CommandNotFound: func(c *cli.Context, command string) {
    85  			handleCommandNotFound(runtime, c, command)
    86  		},
    87  		ExitErrHandler: func(c *cli.Context, err error) {
    88  			handleExitErr(runtime, c, err)
    89  		},
    90  	}
    91  
    92  	// Environment overrides
    93  	overrideFromEnvironment(runtime)
    94  
    95  	// Register internal commands
    96  	registerInternalCommands(runtime)
    97  
    98  	// Register manifest commands
    99  	registerManifestCommands(runtime, options)
   100  
   101  	// Sort commands
   102  	sortCommands(runtime.cli.Commands)
   103  
   104  	return runtime, nil
   105  }
   106  
   107  func initFromEnvironment(runtime *Runtime) error {
   108  	file := environmentOrDefault("CENTRY_FILE", "")
   109  	if file != "" {
   110  		runtime.file = file
   111  		runtime.events = append(runtime.events, fmt.Sprintf("manifest file path set (path=%s source=%s)", runtime.file, "environment"))
   112  	}
   113  	return nil
   114  }
   115  
   116  func initFromArgs(runtime *Runtime, inputArgs []string) error {
   117  	if len(inputArgs) >= 1 && inputArgs[0] == "--centry-file" {
   118  		runtime.file = ""
   119  		if len(inputArgs) >= 2 {
   120  			runtime.file = inputArgs[1]
   121  			runtime.args = inputArgs[2:]
   122  		}
   123  
   124  		if runtime.file == "" {
   125  			return fmt.Errorf("a value must be specified for --centry-file")
   126  		}
   127  
   128  		runtime.events = append(runtime.events, fmt.Sprintf("manifest file path set (path=%s source=%s)", runtime.file, "flag"))
   129  	} else if len(inputArgs) >= 1 && strings.HasPrefix(inputArgs[0], "--centry-file=") {
   130  		flagvalue := strings.Split(inputArgs[0], "=")
   131  		runtime.file = strings.Join(flagvalue[1:], "=")
   132  		runtime.args = inputArgs[1:]
   133  
   134  		if runtime.file == "" {
   135  			return fmt.Errorf("a value must be specified for --centry-file")
   136  		}
   137  
   138  		runtime.events = append(runtime.events, fmt.Sprintf("manifest file path set (path=%s source=%s)", runtime.file, "flag"))
   139  	} else {
   140  		runtime.args = inputArgs
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // Execute runs the CLI and exits with a code
   147  func (runtime *Runtime) Execute() int {
   148  	args := append([]string{""}, runtime.args...)
   149  
   150  	// Run cli
   151  	err := runtime.cli.Run(args)
   152  	if err != nil {
   153  		runtime.context.log.GetLogger().Error(err)
   154  		if strings.HasPrefix(err.Error(), "flag provided but not defined:") {
   155  			return 127
   156  		}
   157  	}
   158  
   159  	// Return exitcode defined in metadata
   160  	if runtime.cli.Metadata[metadataExitCode] != nil {
   161  		switch runtime.cli.Metadata[metadataExitCode].(type) {
   162  		case int:
   163  			return runtime.cli.Metadata[metadataExitCode].(int)
   164  		}
   165  		return 128
   166  	}
   167  
   168  	return 0
   169  }
   170  
   171  func handleBefore(runtime *Runtime, c *cli.Context) error {
   172  	// Override the current log level from options
   173  	logLevel := c.String("centry-config-log-level")
   174  	if c.Bool("centry-quiet") {
   175  		logLevel = "panic"
   176  	}
   177  	runtime.context.log.TrySetLogLevel(logLevel)
   178  
   179  	// Print runtime events
   180  	logger := runtime.context.log.GetLogger()
   181  	for _, e := range runtime.events {
   182  		logger.Debugf("[runtime-event] %s", e)
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  func handleCommandNotFound(runtime *Runtime, c *cli.Context, command string) {
   189  	logger := runtime.context.log.GetLogger()
   190  	logger.WithFields(logrus.Fields{
   191  		"command": command,
   192  	}).Warnf("command not found")
   193  	c.App.Metadata[metadataExitCode] = 127
   194  }
   195  
   196  // Handles errors implementing ExitCoder by printing their
   197  // message and calling OsExiter with the given exit code.
   198  // If the given error instead implements MultiError, each error will be checked
   199  // for the ExitCoder interface, and OsExiter will be called with the last exit
   200  // code found, or exit code 1 if no ExitCoder is found.
   201  func handleExitErr(runtime *Runtime, context *cli.Context, err error) {
   202  	if err == nil {
   203  		return
   204  	}
   205  
   206  	logger := runtime.context.log.GetLogger()
   207  
   208  	if exitErr, ok := err.(cli.ExitCoder); ok {
   209  		if err.Error() != "" {
   210  			if _, ok := exitErr.(cli.ErrorFormatter); ok {
   211  				logger.WithFields(logrus.Fields{
   212  					"command": context.Command.Name,
   213  					"code":    exitErr.ExitCode(),
   214  				}).Errorf("%+v\n", err)
   215  			} else {
   216  				logger.WithFields(logrus.Fields{
   217  					"command": context.Command.Name,
   218  					"code":    exitErr.ExitCode(),
   219  				}).Error(err)
   220  			}
   221  		}
   222  		cli.OsExiter(exitErr.ExitCode())
   223  		return
   224  	}
   225  
   226  	if multiErr, ok := err.(cli.MultiError); ok {
   227  		code := handleMultiError(runtime, context, multiErr)
   228  		cli.OsExiter(code)
   229  		return
   230  	}
   231  }
   232  
   233  func handleMultiError(runtime *Runtime, context *cli.Context, multiErr cli.MultiError) int {
   234  	code := 1
   235  	for _, merr := range multiErr.Errors() {
   236  		if multiErr2, ok := merr.(cli.MultiError); ok {
   237  			code = handleMultiError(runtime, context, multiErr2)
   238  		} else if merr != nil {
   239  			if exitErr, ok := merr.(cli.ExitCoder); ok {
   240  				code = exitErr.ExitCode()
   241  				runtime.context.log.GetLogger().WithFields(logrus.Fields{
   242  					"command": context.Command.Name,
   243  					"code":    code,
   244  				}).Error(merr)
   245  			} else {
   246  				runtime.context.log.GetLogger().WithFields(logrus.Fields{
   247  					"command": context.Command.Name,
   248  				}).Error(merr)
   249  			}
   250  		}
   251  	}
   252  	return code
   253  }