github.com/kata-containers/runtime@v0.0.0-20210505125100-04f29832a923/cli/main.go (about) 1 // Copyright (c) 2014,2015,2016 Docker, Inc. 2 // Copyright (c) 2017-2018 Intel Corporation 3 // 4 // SPDX-License-Identifier: Apache-2.0 5 // 6 7 package main 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "io" 14 "os" 15 "os/signal" 16 goruntime "runtime" 17 "strings" 18 "syscall" 19 20 "github.com/kata-containers/runtime/pkg/katautils" 21 "github.com/kata-containers/runtime/pkg/signals" 22 vc "github.com/kata-containers/runtime/virtcontainers" 23 exp "github.com/kata-containers/runtime/virtcontainers/experimental" 24 vf "github.com/kata-containers/runtime/virtcontainers/factory" 25 tl "github.com/kata-containers/runtime/virtcontainers/factory/template" 26 "github.com/kata-containers/runtime/virtcontainers/pkg/oci" 27 "github.com/kata-containers/runtime/virtcontainers/pkg/rootless" 28 specs "github.com/opencontainers/runtime-spec/specs-go" 29 opentracing "github.com/opentracing/opentracing-go" 30 "github.com/sirupsen/logrus" 31 "github.com/urfave/cli" 32 ) 33 34 // specConfig is the name of the file holding the containers configuration 35 const specConfig = "config.json" 36 37 // arch is the architecture for the running program 38 const arch = goruntime.GOARCH 39 40 var usage = fmt.Sprintf(`%s runtime 41 42 %s is a command line program for running applications packaged 43 according to the Open Container Initiative (OCI).`, name, name) 44 45 var notes = fmt.Sprintf(` 46 NOTES: 47 48 - Commands starting "%s-" and options starting "--%s-" are `+project+` extensions. 49 50 URL: 51 52 The canonical URL for this project is: %s 53 54 `, projectPrefix, projectPrefix, projectURL) 55 56 // kataLog is the logger used to record all messages 57 var kataLog *logrus.Entry 58 59 // originalLoggerLevel is the default log level. It is used to revert the 60 // current log level back to its original value if debug output is not 61 // required. We set the default to 'Warn' for the runtime. 62 var originalLoggerLevel = logrus.WarnLevel 63 64 var debug = false 65 66 // if true, coredump when an internal error occurs or a fatal signal is received 67 var crashOnError = false 68 69 // concrete virtcontainer implementation 70 var virtcontainersImpl = &vc.VCImpl{} 71 72 // vci is used to access a particular virtcontainers implementation. 73 // Normally, it refers to the official package, but is re-assigned in 74 // the tests to allow virtcontainers to be mocked. 75 var vci vc.VC = virtcontainersImpl 76 77 // defaultOutputFile is the default output file to write the gathered 78 // information to. 79 var defaultOutputFile = os.Stdout 80 81 // defaultErrorFile is the default output file to write error 82 // messages to. 83 var defaultErrorFile = os.Stderr 84 85 // runtimeFlags is the list of supported global command-line flags 86 var runtimeFlags = []cli.Flag{ 87 cli.StringFlag{ 88 Name: configFilePathOption, 89 Usage: project + " config file path", 90 }, 91 cli.StringFlag{ 92 Name: "log", 93 Value: "/dev/null", 94 Usage: "set the log file path where internal debug information is written", 95 }, 96 cli.StringFlag{ 97 Name: "log-format", 98 Value: "text", 99 Usage: "set the format used by logs ('text' (default), or 'json')", 100 }, 101 cli.StringFlag{ 102 Name: "root", 103 Value: defaultRootDirectory, 104 Usage: "root directory for storage of container state (this should be located in tmpfs)", 105 }, 106 cli.StringFlag{ 107 Name: "rootless", 108 Value: "auto", 109 Usage: "ignore cgroup permission errors ('true', 'false', or 'auto')", 110 }, 111 cli.BoolFlag{ 112 Name: showConfigPathsOption, 113 Usage: "show config file paths that will be checked for (in order)", 114 }, 115 cli.BoolFlag{ 116 Name: "systemd-cgroup", 117 Usage: "enable systemd cgroup support, expects cgroupsPath to be of form \"slice:prefix:name\" for e.g. \"system.slice:runc:434234\"", 118 }, 119 } 120 121 // runtimeCommands is the list of supported command-line (sub-) 122 // commands. 123 var runtimeCommands = []cli.Command{ 124 createCLICommand, 125 deleteCLICommand, 126 execCLICommand, 127 killCLICommand, 128 listCLICommand, 129 pauseCLICommand, 130 psCLICommand, 131 resumeCLICommand, 132 runCLICommand, 133 specCLICommand, 134 startCLICommand, 135 stateCLICommand, 136 updateCLICommand, 137 eventsCLICommand, 138 versionCLICommand, 139 140 // Kata Containers specific extensions 141 kataCheckCLICommand, 142 kataEnvCLICommand, 143 kataNetworkCLICommand, 144 kataOverheadCLICommand, 145 factoryCLICommand, 146 } 147 148 // runtimeBeforeSubcommands is the function to run before command-line 149 // parsing occurs. 150 var runtimeBeforeSubcommands = beforeSubcommands 151 152 // runtimeAfterSubcommands is the function to run after the command-line 153 // has been parsed. 154 var runtimeAfterSubcommands = afterSubcommands 155 156 // runtimeCommandNotFound is the function to handle an invalid sub-command. 157 var runtimeCommandNotFound = commandNotFound 158 159 // runtimeVersion is the function that returns the full version 160 // string describing the runtime. 161 var runtimeVersion = makeVersionString 162 163 // saved default cli package values (for testing). 164 var savedCLIAppHelpTemplate = cli.AppHelpTemplate 165 var savedCLIVersionPrinter = cli.VersionPrinter 166 var savedCLIErrWriter = cli.ErrWriter 167 168 func init() { 169 kataLog = logrus.WithFields(logrus.Fields{ 170 "name": name, 171 "source": "runtime", 172 "arch": arch, 173 "pid": os.Getpid(), 174 }) 175 176 // Save the original log level and then set to debug level to ensure 177 // that any problems detected before the config file is parsed are 178 // logged. This is required since the config file determines the true 179 // log level for the runtime: once parsed the log level is set 180 // appropriately but for issues between now and completion of the 181 // config file parsing, it is prudent to operate in verbose mode. 182 originalLoggerLevel = kataLog.Logger.Level 183 kataLog.Logger.Level = logrus.DebugLevel 184 } 185 186 // setupSignalHandler sets up signal handling, starting a go routine to deal 187 // with signals as they arrive. 188 // 189 // Note that the specified context is NOT used to create a trace span (since the 190 // first (root) span must be created in beforeSubcommands()): it is simply 191 // used to pass to the crash handling functions to finalise tracing. 192 func setupSignalHandler(ctx context.Context) { 193 signals.SetLogger(kataLog) 194 195 sigCh := make(chan os.Signal, 8) 196 197 for _, sig := range signals.HandledSignals() { 198 signal.Notify(sigCh, sig) 199 } 200 201 dieCb := func() { 202 katautils.StopTracing(ctx) 203 } 204 205 go func() { 206 for { 207 sig := <-sigCh 208 209 nativeSignal, ok := sig.(syscall.Signal) 210 if !ok { 211 err := errors.New("unknown signal") 212 kataLog.WithError(err).WithField("signal", sig.String()).Error() 213 continue 214 } 215 216 if signals.FatalSignal(nativeSignal) { 217 kataLog.WithField("signal", sig).Error("received fatal signal") 218 signals.Die(dieCb) 219 } else if debug && signals.NonFatalSignal(nativeSignal) { 220 kataLog.WithField("signal", sig).Debug("handling signal") 221 signals.Backtrace() 222 } 223 } 224 }() 225 } 226 227 // setExternalLoggers registers the specified logger with the external 228 // packages which accept a logger to handle their own logging. 229 func setExternalLoggers(ctx context.Context, logger *logrus.Entry) { 230 var span opentracing.Span 231 232 // Only create a new span if a root span already exists. This is 233 // required to ensure that this function will not disrupt the root 234 // span logic by creating a span before the proper root span has been 235 // created. 236 237 if opentracing.SpanFromContext(ctx) != nil { 238 span, ctx = katautils.Trace(ctx, "setExternalLoggers") 239 defer span.Finish() 240 } 241 242 // Set virtcontainers logger. 243 vci.SetLogger(ctx, logger) 244 245 // Set vm factory logger. 246 vf.SetLogger(ctx, logger) 247 248 // Set vm factory template logger. 249 tl.SetLogger(ctx, logger) 250 251 // Set the OCI package logger. 252 oci.SetLogger(ctx, logger) 253 254 // Set the katautils package logger 255 katautils.SetLogger(ctx, logger, originalLoggerLevel) 256 257 // Set the rootless package logger 258 rootless.SetLogger(ctx, logger) 259 } 260 261 // beforeSubcommands is the function to perform preliminary checks 262 // before command-line parsing occurs. 263 func beforeSubcommands(c *cli.Context) error { 264 var configFile string 265 var runtimeConfig oci.RuntimeConfig 266 var err error 267 268 katautils.SetConfigOptions(name, defaultRuntimeConfiguration, defaultSysConfRuntimeConfiguration) 269 270 handleShowConfig(c) 271 272 if userWantsUsage(c) { 273 // No setup required if the user just 274 // wants to see the usage statement. 275 return nil 276 } 277 278 r, err := parseBoolOrAuto(c.GlobalString("rootless")) 279 if err != nil { 280 return err 281 } 282 // If flag is true/false, assign the rootless flag. 283 // vc will not perform any auto-detection in that case. 284 // In case flag is nil or auto, vc detects if the runtime is running as rootless. 285 if r != nil { 286 rootless.SetRootless(*r) 287 } 288 // Support --systed-cgroup 289 // Issue: https://github.com/kata-containers/runtime/issues/2428 290 291 ignoreConfigLogs := false 292 var traceRootSpan string 293 294 subCmdIsCheckCmd := (c.NArg() >= 1 && (c.Args()[0] == checkCmd)) 295 if subCmdIsCheckCmd { 296 // checkCmd will use the default logrus logger to stderr 297 // raise the logger default level to warn 298 kataLog.Logger.SetLevel(logrus.WarnLevel) 299 // do not print configuration logs for checkCmd 300 ignoreConfigLogs = true 301 } else { 302 if path := c.GlobalString("log"); path != "" { 303 f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0640) 304 if err != nil { 305 return err 306 } 307 kataLog.Logger.Out = f 308 } 309 310 switch c.GlobalString("log-format") { 311 case "text": 312 // retain logrus's default. 313 case "json": 314 kataLog.Logger.Formatter = new(logrus.JSONFormatter) 315 default: 316 return fmt.Errorf("unknown log-format %q", c.GlobalString("log-format")) 317 } 318 319 // Add the name of the sub-command to each log entry for easier 320 // debugging. 321 cmdName := c.Args().First() 322 if c.App.Command(cmdName) != nil { 323 kataLog = kataLog.WithField("command", cmdName) 324 325 // Name for the root span (used for tracing) now the 326 // sub-command name is known. 327 traceRootSpan = name + " " + cmdName 328 } 329 330 // Since a context is required, pass a new (throw-away) one - we 331 // cannot use the main context as tracing hasn't been enabled yet 332 // (meaning any spans created at this point will be silently ignored). 333 setExternalLoggers(context.Background(), kataLog) 334 335 if c.NArg() == 1 && c.Args()[0] == envCmd { 336 // simply report the logging setup 337 ignoreConfigLogs = true 338 } 339 } 340 341 configFile, runtimeConfig, err = katautils.LoadConfiguration(c.GlobalString(configFilePathOption), ignoreConfigLogs, false) 342 if err != nil { 343 fatal(err) 344 } 345 if !subCmdIsCheckCmd { 346 debug = runtimeConfig.Debug 347 crashOnError = runtimeConfig.Debug 348 349 if traceRootSpan != "" { 350 // Create the tracer. 351 // 352 // Note: no spans are created until the command-line has been parsed. 353 // This delays collection of trace data slightly but benefits the user by 354 // ensuring the first span is the name of the sub-command being 355 // invoked from the command-line. 356 err = setupTracing(c, traceRootSpan) 357 if err != nil { 358 return err 359 } 360 } 361 } 362 363 args := strings.Join(c.Args(), " ") 364 365 fields := logrus.Fields{ 366 "version": version, 367 "commit": commit, 368 "arguments": `"` + args + `"`, 369 } 370 371 err = addExpFeatures(c, runtimeConfig) 372 if err != nil { 373 return err 374 } 375 376 kataLog.WithFields(fields).Info() 377 378 // make the data accessible to the sub-commands. 379 c.App.Metadata["runtimeConfig"] = runtimeConfig 380 c.App.Metadata["configFile"] = configFile 381 382 return nil 383 } 384 385 // handleShowConfig determines if the user wishes to see the configuration 386 // paths. If so, it will display them and then exit. 387 func handleShowConfig(context *cli.Context) { 388 if context.GlobalBool(showConfigPathsOption) { 389 files := katautils.GetDefaultConfigFilePaths() 390 391 for _, file := range files { 392 fmt.Fprintf(defaultOutputFile, "%s\n", file) 393 } 394 395 exit(0) 396 } 397 } 398 399 func setupTracing(context *cli.Context, rootSpanName string) error { 400 tracer, err := katautils.CreateTracer(name) 401 if err != nil { 402 fatal(err) 403 } 404 405 // Create the root span now that the sub-command name is 406 // known. 407 // 408 // Note that this "Before" function is called (and returns) 409 // before the subcommand handler is called. As such, we cannot 410 // "Finish()" the span here - that is handled in the .After 411 // function. 412 span := tracer.StartSpan(rootSpanName) 413 414 ctx, err := cliContextToContext(context) 415 if err != nil { 416 return err 417 } 418 419 span.SetTag("subsystem", "runtime") 420 421 // Associate the root span with the context 422 ctx = opentracing.ContextWithSpan(ctx, span) 423 424 // Add tracer to metadata and update the context 425 context.App.Metadata["tracer"] = tracer 426 context.App.Metadata["context"] = ctx 427 428 return nil 429 } 430 431 // add supported experimental features in context 432 func addExpFeatures(clictx *cli.Context, runtimeConfig oci.RuntimeConfig) error { 433 ctx, err := cliContextToContext(clictx) 434 if err != nil { 435 return err 436 } 437 438 var exps []string 439 for _, e := range runtimeConfig.Experimental { 440 exps = append(exps, e.Name) 441 } 442 443 ctx = exp.ContextWithExp(ctx, exps) 444 // Add tracer to metadata and update the context 445 clictx.App.Metadata["context"] = ctx 446 return nil 447 } 448 449 func afterSubcommands(c *cli.Context) error { 450 ctx, err := cliContextToContext(c) 451 if err != nil { 452 return err 453 } 454 455 katautils.StopTracing(ctx) 456 457 return nil 458 } 459 460 // function called when an invalid command is specified which causes the 461 // runtime to error. 462 func commandNotFound(c *cli.Context, command string) { 463 err := fmt.Errorf("Invalid command %q", command) 464 fatal(err) 465 } 466 467 // makeVersionString returns a multi-line string describing the runtime 468 // version along with the version of the OCI specification it supports. 469 func makeVersionString() string { 470 v := make([]string, 0, 3) 471 472 versionStr := version 473 if versionStr == "" { 474 versionStr = unknown 475 } 476 477 v = append(v, name+" : "+versionStr) 478 479 commitStr := commit 480 if commitStr == "" { 481 commitStr = unknown 482 } 483 484 v = append(v, " commit : "+commitStr) 485 486 specVersionStr := specs.Version 487 if specVersionStr == "" { 488 specVersionStr = unknown 489 } 490 491 v = append(v, " OCI specs: "+specVersionStr) 492 493 return strings.Join(v, "\n") 494 } 495 496 // setCLIGlobals modifies various cli package global variables 497 func setCLIGlobals() { 498 cli.AppHelpTemplate = fmt.Sprintf(`%s%s`, cli.AppHelpTemplate, notes) 499 500 // Override the default function to display version details to 501 // ensure the "--version" option and "version" command are identical. 502 cli.VersionPrinter = func(c *cli.Context) { 503 fmt.Fprintln(defaultOutputFile, c.App.Version) 504 } 505 506 // If the command returns an error, cli takes upon itself to print 507 // the error on cli.ErrWriter and exit. 508 // Use our own writer here to ensure the log gets sent to the right 509 // location. 510 cli.ErrWriter = &fatalWriter{cli.ErrWriter} 511 } 512 513 // createRuntimeApp creates an application to process the command-line 514 // arguments and invoke the requested runtime command. 515 func createRuntimeApp(ctx context.Context, args []string) error { 516 app := cli.NewApp() 517 518 app.Name = name 519 app.Writer = defaultOutputFile 520 app.Usage = usage 521 app.CommandNotFound = runtimeCommandNotFound 522 app.Version = runtimeVersion() 523 app.Flags = runtimeFlags 524 app.Commands = runtimeCommands 525 app.Before = runtimeBeforeSubcommands 526 app.After = runtimeAfterSubcommands 527 app.EnableBashCompletion = true 528 529 // allow sub-commands to access context 530 app.Metadata = map[string]interface{}{ 531 "context": ctx, 532 } 533 534 return app.Run(args) 535 } 536 537 // userWantsUsage determines if the user only wishes to see the usage 538 // statement. 539 func userWantsUsage(context *cli.Context) bool { 540 if context.NArg() == 0 { 541 return true 542 } 543 544 if context.NArg() == 1 && (context.Args()[0] == "help" || context.Args()[0] == "version") { 545 return true 546 } 547 548 if context.NArg() >= 2 && (context.Args()[1] == "-h" || context.Args()[1] == "--help") { 549 return true 550 } 551 552 return false 553 } 554 555 // fatal prints the error's details exits the program. 556 func fatal(err error) { 557 kataLog.Error(err) 558 fmt.Fprintln(defaultErrorFile, err) 559 exit(1) 560 } 561 562 type fatalWriter struct { 563 cliErrWriter io.Writer 564 } 565 566 func (f *fatalWriter) Write(p []byte) (n int, err error) { 567 // Ensure error is logged before displaying to the user 568 kataLog.Error(string(p)) 569 return f.cliErrWriter.Write(p) 570 } 571 572 func createRuntime(ctx context.Context) { 573 setupSignalHandler(ctx) 574 575 setCLIGlobals() 576 577 err := createRuntimeApp(ctx, os.Args) 578 if err != nil { 579 fatal(err) 580 } 581 } 582 583 // cliContextToContext extracts the generic context from the specified 584 // cli context. 585 func cliContextToContext(c *cli.Context) (context.Context, error) { 586 if c == nil { 587 return nil, errors.New("need cli.Context") 588 } 589 590 // extract the main context 591 ctx, ok := c.App.Metadata["context"].(context.Context) 592 if !ok { 593 return nil, errors.New("invalid or missing context in metadata") 594 } 595 596 return ctx, nil 597 } 598 599 func main() { 600 // create a new empty context 601 ctx := context.Background() 602 603 dieCb := func() { 604 katautils.StopTracing(ctx) 605 } 606 607 defer signals.HandlePanic(dieCb) 608 609 createRuntime(ctx) 610 }