github.com/apptainer/singularity@v3.1.1+incompatible/cmd/internal/cli/singularity.go (about)

     1  // Copyright (c) 2018, Sylabs Inc. All rights reserved.
     2  // This software is licensed under a 3-clause BSD license. Please consult the
     3  // LICENSE.md file distributed with the sources of this project regarding your
     4  // rights to use or distribute this software.
     5  
     6  package cli
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"os/user"
    13  	"path"
    14  	"strings"
    15  	"text/template"
    16  
    17  	"github.com/spf13/cobra"
    18  	"github.com/spf13/pflag"
    19  	"github.com/sylabs/singularity/docs"
    20  	"github.com/sylabs/singularity/internal/pkg/buildcfg"
    21  	"github.com/sylabs/singularity/internal/pkg/sylog"
    22  	"github.com/sylabs/singularity/internal/pkg/util/auth"
    23  )
    24  
    25  // Global variables for singularity CLI
    26  var (
    27  	debug   bool
    28  	silent  bool
    29  	verbose bool
    30  	quiet   bool
    31  )
    32  
    33  var (
    34  	// TokenFile holds the path to the sylabs auth token file
    35  	defaultTokenFile, tokenFile string
    36  	// authToken holds the sylabs auth token
    37  	authToken, authWarning string
    38  )
    39  
    40  const (
    41  	envPrefix = "SINGULARITY_"
    42  )
    43  
    44  func init() {
    45  	SingularityCmd.Flags().SetInterspersed(false)
    46  	SingularityCmd.PersistentFlags().SetInterspersed(false)
    47  
    48  	templateFuncs := template.FuncMap{
    49  		"TraverseParentsUses": TraverseParentsUses,
    50  	}
    51  	cobra.AddTemplateFuncs(templateFuncs)
    52  
    53  	SingularityCmd.SetHelpTemplate(docs.HelpTemplate)
    54  	SingularityCmd.SetUsageTemplate(docs.UseTemplate)
    55  
    56  	vt := fmt.Sprintf("%s version {{printf \"%%s\" .Version}}\n", buildcfg.PACKAGE_NAME)
    57  	SingularityCmd.SetVersionTemplate(vt)
    58  
    59  	usr, err := user.Current()
    60  	if err != nil {
    61  		sylog.Fatalf("Couldn't determine user home directory: %v", err)
    62  	}
    63  	defaultTokenFile = path.Join(usr.HomeDir, ".singularity", "sylabs-token")
    64  
    65  	SingularityCmd.Flags().BoolVarP(&debug, "debug", "d", false, "print debugging information (highest verbosity)")
    66  	SingularityCmd.Flags().BoolVarP(&silent, "silent", "s", false, "only print errors")
    67  	SingularityCmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress normal output")
    68  	SingularityCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "print additional information")
    69  	SingularityCmd.Flags().StringVarP(&tokenFile, "tokenfile", "t", defaultTokenFile, "path to the file holding your sylabs authentication token")
    70  
    71  	VersionCmd.Flags().SetInterspersed(false)
    72  	SingularityCmd.AddCommand(VersionCmd)
    73  }
    74  
    75  func setSylogMessageLevel(cmd *cobra.Command, args []string) {
    76  	var level int
    77  
    78  	if debug {
    79  		level = 5
    80  	} else if verbose {
    81  		level = 4
    82  	} else if quiet {
    83  		level = -1
    84  	} else if silent {
    85  		level = -3
    86  	} else {
    87  		level = 1
    88  	}
    89  
    90  	sylog.SetLevel(level)
    91  }
    92  
    93  // SingularityCmd is the base command when called without any subcommands
    94  var SingularityCmd = &cobra.Command{
    95  	TraverseChildren:      true,
    96  	DisableFlagsInUseLine: true,
    97  	PersistentPreRun:      persistentPreRun,
    98  	RunE: func(cmd *cobra.Command, args []string) error {
    99  		return errors.New("Invalid command")
   100  	},
   101  
   102  	Use:           docs.SingularityUse,
   103  	Version:       buildcfg.PACKAGE_VERSION,
   104  	Short:         docs.SingularityShort,
   105  	Long:          docs.SingularityLong,
   106  	Example:       docs.SingularityExample,
   107  	SilenceErrors: true,
   108  }
   109  
   110  // ExecuteSingularity adds all child commands to the root command and sets
   111  // flags appropriately. This is called by main.main(). It only needs to happen
   112  // once to the root command (singularity).
   113  func ExecuteSingularity() {
   114  	defaultEnv := "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
   115  
   116  	// backup user PATH
   117  	userEnv := strings.Join([]string{os.Getenv("PATH"), defaultEnv}, ":")
   118  	os.Setenv("USER_PATH", userEnv)
   119  
   120  	os.Setenv("PATH", defaultEnv)
   121  	if err := SingularityCmd.Execute(); err != nil {
   122  		os.Exit(1)
   123  	}
   124  }
   125  
   126  // TraverseParentsUses walks the parent commands and outputs a properly formatted use string
   127  func TraverseParentsUses(cmd *cobra.Command) string {
   128  	if cmd.HasParent() {
   129  		return TraverseParentsUses(cmd.Parent()) + cmd.Use + " "
   130  	}
   131  
   132  	return cmd.Use + " "
   133  }
   134  
   135  // VersionCmd displays installed singularity version
   136  var VersionCmd = &cobra.Command{
   137  	DisableFlagsInUseLine: true,
   138  	Run: func(cmd *cobra.Command, args []string) {
   139  		fmt.Println(buildcfg.PACKAGE_VERSION)
   140  	},
   141  
   142  	Use:   "version",
   143  	Short: "Show the version for Singularity",
   144  }
   145  
   146  func updateFlagsFromEnv(cmd *cobra.Command) {
   147  	cmd.Flags().VisitAll(handleEnv)
   148  }
   149  
   150  func handleEnv(flag *pflag.Flag) {
   151  	envKeys, ok := flag.Annotations["envkey"]
   152  	if !ok {
   153  		return
   154  	}
   155  
   156  	for _, key := range envKeys {
   157  		val, set := os.LookupEnv(envPrefix + key)
   158  		if !set {
   159  			continue
   160  		}
   161  
   162  		updateFn := flagEnvFuncs[flag.Name]
   163  		updateFn(flag, val)
   164  	}
   165  
   166  }
   167  
   168  func persistentPreRun(cmd *cobra.Command, args []string) {
   169  	setSylogMessageLevel(cmd, args)
   170  	updateFlagsFromEnv(cmd)
   171  }
   172  
   173  // sylabsToken process the authentication Token
   174  // priority default_file < env < file_flag
   175  func sylabsToken(cmd *cobra.Command, args []string) {
   176  	if val := os.Getenv("SYLABS_TOKEN"); val != "" {
   177  		authToken = val
   178  	}
   179  	if tokenFile != defaultTokenFile {
   180  		authToken, authWarning = auth.ReadToken(tokenFile)
   181  	}
   182  	if authToken == "" {
   183  		authToken, authWarning = auth.ReadToken(defaultTokenFile)
   184  	}
   185  	if authToken == "" && authWarning == auth.WarningTokenFileNotFound {
   186  		sylog.Warningf("%v : Only pulls of public images will succeed", authWarning)
   187  	}
   188  }
   189  
   190  // envAppend combines command line and environment var into a single argument
   191  func envAppend(flag *pflag.Flag, envvar string) {
   192  	if err := flag.Value.Set(envvar); err != nil {
   193  		sylog.Warningf("Unable to set %s to environment variable value %s", flag.Name, envvar)
   194  	} else {
   195  		flag.Changed = true
   196  		sylog.Debugf("Update flag Value to: %s", flag.Value)
   197  	}
   198  }
   199  
   200  // envBool sets a bool flag if the CLI option is unset and env var is set
   201  func envBool(flag *pflag.Flag, envvar string) {
   202  	if flag.Changed == true || envvar == "" {
   203  		return
   204  	}
   205  
   206  	if err := flag.Value.Set(envvar); err != nil {
   207  		sylog.Debugf("Unable to set flag %s to value %s: %s", flag.Name, envvar, err)
   208  		if err := flag.Value.Set("true"); err != nil {
   209  			sylog.Warningf("Unable to set flag %s to value %s: %s", flag.Name, envvar, err)
   210  			return
   211  		}
   212  	}
   213  
   214  	flag.Changed = true
   215  	sylog.Debugf("Set %s Value to: %s", flag.Name, flag.Value)
   216  }
   217  
   218  // envStringNSlice writes to a string or slice flag if CLI option/argument
   219  // string is unset and env var is set
   220  func envStringNSlice(flag *pflag.Flag, envvar string) {
   221  	if flag.Changed == true {
   222  		return
   223  	}
   224  
   225  	if err := flag.Value.Set(envvar); err != nil {
   226  		sylog.Warningf("Unable to set flag %s to value %s: %s", flag.Name, envvar, err)
   227  		return
   228  	}
   229  
   230  	flag.Changed = true
   231  	sylog.Debugf("Set %s Value to: %s", flag.Name, flag.Value)
   232  }
   233  
   234  type envHandle func(*pflag.Flag, string)
   235  
   236  // map of functions to use to bind flags to environment variables
   237  var flagEnvFuncs = map[string]envHandle{
   238  	// action flags
   239  	"bind":          envAppend,
   240  	"home":          envStringNSlice,
   241  	"overlay":       envStringNSlice,
   242  	"scratch":       envStringNSlice,
   243  	"workdir":       envStringNSlice,
   244  	"shell":         envStringNSlice,
   245  	"pwd":           envStringNSlice,
   246  	"hostname":      envStringNSlice,
   247  	"network":       envStringNSlice,
   248  	"network-args":  envStringNSlice,
   249  	"dns":           envStringNSlice,
   250  	"containlibs":   envStringNSlice,
   251  	"security":      envStringNSlice,
   252  	"apply-cgroups": envStringNSlice,
   253  	"app":           envStringNSlice,
   254  
   255  	"boot":           envBool,
   256  	"fakeroot":       envBool,
   257  	"cleanenv":       envBool,
   258  	"contain":        envBool,
   259  	"containall":     envBool,
   260  	"nv":             envBool,
   261  	"no-nv":          envBool,
   262  	"writable":       envBool,
   263  	"writable-tmpfs": envBool,
   264  	"no-home":        envBool,
   265  	"no-init":        envBool,
   266  
   267  	"pid":    envBool,
   268  	"ipc":    envBool,
   269  	"net":    envBool,
   270  	"uts":    envBool,
   271  	"userns": envBool,
   272  
   273  	"keep-privs":   envBool,
   274  	"no-privs":     envBool,
   275  	"add-caps":     envStringNSlice,
   276  	"drop-caps":    envStringNSlice,
   277  	"allow-setuid": envBool,
   278  
   279  	// build flags
   280  	"sandbox": envBool,
   281  	"section": envStringNSlice,
   282  	"json":    envBool,
   283  	"name":    envStringNSlice,
   284  	// "writable": envBool, // set above for now
   285  	"force":           envBool,
   286  	"update":          envBool,
   287  	"notest":          envBool,
   288  	"remote":          envBool,
   289  	"detached":        envBool,
   290  	"builder":         envStringNSlice,
   291  	"library":         envStringNSlice,
   292  	"nohttps":         envBool,
   293  	"no-cleanup":      envBool,
   294  	"tmpdir":          envStringNSlice,
   295  	"docker-username": envStringNSlice,
   296  	"docker-password": envStringNSlice,
   297  	"docker-login":    envBool,
   298  
   299  	// capability flags (and others)
   300  	"user":  envStringNSlice,
   301  	"group": envStringNSlice,
   302  	"desc":  envBool,
   303  	"all":   envBool,
   304  
   305  	// instance flags
   306  	"signal": envStringNSlice,
   307  
   308  	// keys flags
   309  	"secret": envBool,
   310  	"url":    envStringNSlice,
   311  
   312  	// inspect flags
   313  	"labels":      envBool,
   314  	"deffile":     envBool,
   315  	"runscript":   envBool,
   316  	"test":        envBool,
   317  	"environment": envBool,
   318  	"helpfile":    envBool,
   319  }