github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/cmd/umoci/main.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 SUSE LLC
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package main
    19  
    20  import (
    21  	"os"
    22  	"runtime/pprof"
    23  
    24  	"github.com/apex/log"
    25  	logcli "github.com/apex/log/handlers/cli"
    26  	"github.com/opencontainers/umoci"
    27  	"github.com/pkg/errors"
    28  	"github.com/urfave/cli"
    29  )
    30  
    31  const (
    32  	usage = `umoci modifies Open Container images`
    33  
    34  	// Categories used to automatically monkey-patch flags to commands.
    35  	categoryLayout = "layout"
    36  	categoryImage  = "image"
    37  )
    38  
    39  // Main is the underlying main() implementation. You can call this directly as
    40  // though it were the command-line arguments of the umoci binary (this is
    41  // needed for umoci's integration test hacks you can find in main_test.go).
    42  func Main(args []string) error {
    43  	app := cli.NewApp()
    44  	app.Name = "umoci"
    45  	app.Usage = usage
    46  	app.Authors = []cli.Author{
    47  		{
    48  			Name:  "Aleksa Sarai",
    49  			Email: "asarai@suse.com",
    50  		},
    51  	}
    52  	app.Version = umoci.FullVersion()
    53  
    54  	app.Flags = []cli.Flag{
    55  		cli.BoolFlag{
    56  			Name:  "verbose",
    57  			Usage: "alias for --log=info",
    58  		},
    59  		cli.StringFlag{
    60  			Name:  "log",
    61  			Usage: "set the log level (debug, info, [warn], error, fatal)",
    62  			Value: "warn",
    63  		},
    64  		cli.StringFlag{
    65  			Name:   "cpu-profile",
    66  			Usage:  "profile umoci during execution and output it to a file",
    67  			Hidden: true,
    68  		},
    69  	}
    70  
    71  	app.Before = func(ctx *cli.Context) error {
    72  		log.SetHandler(logcli.New(os.Stderr))
    73  
    74  		if ctx.GlobalBool("verbose") {
    75  			if ctx.GlobalIsSet("log") {
    76  				return errors.New("--log=* and --verbose are mutually exclusive")
    77  			}
    78  			if err := ctx.GlobalSet("log", "info"); err != nil {
    79  				// Should _never_ be reached.
    80  				return errors.Wrap(err, "[internal error] failure auto-setting --log=info")
    81  			}
    82  		}
    83  		level, err := log.ParseLevel(ctx.GlobalString("log"))
    84  		if err != nil {
    85  			return errors.Wrap(err, "parsing log level")
    86  		}
    87  		log.SetLevel(level)
    88  
    89  		if path := ctx.GlobalString("cpu-profile"); path != "" {
    90  			fh, err := os.Create(path)
    91  			if err != nil {
    92  				return errors.Wrap(err, "opening cpu-profile path")
    93  			}
    94  			if err := pprof.StartCPUProfile(fh); err != nil {
    95  				return errors.Wrap(err, "start cpu-profile")
    96  			}
    97  		}
    98  		return nil
    99  	}
   100  
   101  	app.After = func(ctx *cli.Context) error {
   102  		pprof.StopCPUProfile()
   103  		return nil
   104  	}
   105  
   106  	app.Commands = []cli.Command{
   107  		configCommand,
   108  		unpackCommand,
   109  		repackCommand,
   110  		gcCommand,
   111  		initCommand,
   112  		newCommand,
   113  		tagAddCommand,
   114  		tagRemoveCommand,
   115  		tagListCommand,
   116  		statCommand,
   117  		rawSubcommand,
   118  		insertCommand,
   119  	}
   120  
   121  	app.Metadata = map[string]interface{}{}
   122  
   123  	// In order to make the uxXyz wrappers not too cumbersome we automatically
   124  	// add them to images with categories set to categoryImage or
   125  	// categoryLayout. Monkey patching was never this neat.
   126  	for _, cmd := range flattenCommands(app.Commands) {
   127  		switch cmd.Category {
   128  		case categoryImage:
   129  			oldBefore := cmd.Before
   130  			cmd.Before = func(ctx *cli.Context) error {
   131  				if _, ok := ctx.App.Metadata["--image-path"]; !ok {
   132  					return errors.Errorf("missing mandatory argument: --image")
   133  				}
   134  				if _, ok := ctx.App.Metadata["--image-tag"]; !ok {
   135  					return errors.Errorf("missing mandatory argument: --image")
   136  				}
   137  				if oldBefore != nil {
   138  					return oldBefore(ctx)
   139  				}
   140  				return nil
   141  			}
   142  			*cmd = uxImage(*cmd)
   143  		case categoryLayout:
   144  			oldBefore := cmd.Before
   145  			cmd.Before = func(ctx *cli.Context) error {
   146  				if _, ok := ctx.App.Metadata["--image-path"]; !ok {
   147  					return errors.Errorf("missing mandatory argument: --layout")
   148  				}
   149  				if oldBefore != nil {
   150  					return oldBefore(ctx)
   151  				}
   152  				return nil
   153  			}
   154  			*cmd = uxLayout(*cmd)
   155  		}
   156  	}
   157  
   158  	err := app.Run(args)
   159  	if err != nil {
   160  		// If an error is a permission based error, give a hint to the user
   161  		// that --rootless might help. We probably should only be doing this if
   162  		// we're an unprivileged user.
   163  		if os.IsPermission(errors.Cause(err)) {
   164  			log.Warn("umoci encountered a permission error: maybe --rootless will help?")
   165  		}
   166  		log.Debugf("%+v", err)
   167  	}
   168  	return err
   169  }
   170  
   171  func main() {
   172  	if err := Main(os.Args); err != nil {
   173  		log.Fatalf("%v", err)
   174  	}
   175  }