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 }