diff --git a/build b/build index cfb8c85..e7aa80e 100755 --- a/build +++ b/build @@ -13,8 +13,8 @@ export GOPATH=${GOPATH}:${PWD}/gopath eval $(go env) -echo "Building rkt (stage0)..." -go build -o $GOBIN/rkt ${REPO_PATH}/stage0 +echo "Building rkt..." +go build -o $GOBIN/rkt ${REPO_PATH}/cmd echo "Building init (stage1)..." go build -o $GOBIN/init ${REPO_PATH}/stage1 diff --git a/cmd/help.go b/cmd/help.go new file mode 100644 index 0000000..3116da4 --- /dev/null +++ b/cmd/help.go @@ -0,0 +1,127 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + "text/template" + + "github.com/coreos-inc/rkt/rkt" +) + +var ( + cmdHelp = &Command{ + Name: "help", + Summary: "Show a list of commands or help for one command", + Usage: "[COMMAND]", + Description: "Show a list of commands or detailed help for one command", + Run: runHelp, + } + + globalUsageTemplate *template.Template + commandUsageTemplate *template.Template + templFuncs = template.FuncMap{ + "descToLines": func(s string) []string { + // trim leading/trailing whitespace and split into slice of lines + return strings.Split(strings.Trim(s, "\n\t "), "\n") + }, + "printOption": func(name, defvalue, usage string) string { + prefix := "--" + if len(name) == 1 { + prefix = "-" + } + return fmt.Sprintf("\t%s%s=%s\t%s", prefix, name, defvalue, usage) + }, + } +) + +func init() { + globalUsageTemplate = template.Must(template.New("global_usage").Funcs(templFuncs).Parse(` +NAME: +{{printf "\t%s - %s" .Executable .Description}} + +USAGE: +{{printf "\t%s" .Executable}} [global options] [command options] [arguments...] + +VERSION: +{{printf "\t%s" .Version}} + +COMMANDS:{{range .Commands}} +{{printf "\t%s\t%s" .Name .Summary}}{{end}} + +GLOBAL OPTIONS:{{range .Flags}} +{{printOption .Name .DefValue .Usage}}{{end}} + +Run "{{.Executable}} help " for more details on a specific command. +`[1:])) + commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(` +NAME: +{{printf "\t%s - %s" .Cmd.Name .Cmd.Summary}} + +USAGE: +{{printf "\t%s %s %s" .Executable .Cmd.Name .Cmd.Usage}} + +DESCRIPTION: +{{range $line := descToLines .Cmd.Description}}{{printf "\t%s" $line}} +{{end}} +{{if .CmdFlags}}OPTIONS:{{range .CmdFlags}} +{{printOption .Name .DefValue .Usage}}{{end}} + +{{end}}For help on global options run "{{.Executable}} help" +`[1:])) +} + +func runHelp(args []string) (exit int) { + if len(args) < 1 { + printGlobalUsage() + return + } + + var cmd *Command + + for _, c := range commands { + if c.Name == args[0] { + cmd = c + break + } + } + + if cmd == nil { + fmt.Fprintf(os.Stderr, "Unrecognized command: %s\n", args[0]) + return 1 + } + + printCommandUsage(cmd) + return +} + +func printGlobalUsage() { + globalUsageTemplate.Execute(out, struct { + Executable string + Commands []*Command + Flags []*flag.Flag + Description string + Version string + }{ + cliName, + commands, + getAllFlags(), + cliDescription, + rkt.Version, + }) + out.Flush() +} + +func printCommandUsage(cmd *Command) { + commandUsageTemplate.Execute(out, struct { + Executable string + Cmd *Command + CmdFlags []*flag.Flag + }{ + cliName, + cmd, + getFlags(&cmd.Flags), + }) + out.Flush() +} diff --git a/cmd/rkt.go b/cmd/rkt.go new file mode 100644 index 0000000..3cccad2 --- /dev/null +++ b/cmd/rkt.go @@ -0,0 +1,93 @@ +package main + +import ( + "flag" + "fmt" + "os" + "text/tabwriter" +) + +const ( + cliName = "rkt" + cliDescription = "rkt, the application container runner" +) + +var ( + globalFlagset = flag.NewFlagSet(cliName, flag.ExitOnError) + out *tabwriter.Writer + commands []*Command + globalFlags = struct { + Dir string + Debug bool + Help bool + }{} +) + +func init() { + globalFlagset.BoolVar(&globalFlags.Help, "help", false, "Print usage information and exit") + globalFlagset.BoolVar(&globalFlags.Debug, "debug", false, "Print out more debug information to stderr") + globalFlagset.StringVar(&globalFlags.Dir, "dir", "", "rocket data directory") +} + +type Command struct { + Name string // Name of the Command and the string to use to invoke it + Summary string // One-sentence summary of what the Command does + Usage string // Usage options/arguments + Description string // Detailed description of command + Flags flag.FlagSet // Set of flags associated with this command + + Run func(args []string) int // Run a command with the given arguments, return exit status + +} + +func init() { + out = new(tabwriter.Writer) + out.Init(os.Stdout, 0, 8, 1, '\t', 0) + commands = []*Command{ + cmdHelp, + cmdRun, + cmdVersion, + } +} + +func main() { + // parse global arguments + globalFlagset.Parse(os.Args[1:]) + args := globalFlagset.Args() + if len(args) < 1 || globalFlags.Help { + args = []string{"help"} + } + + var cmd *Command + + // determine which Command should be run + for _, c := range commands { + if c.Name == args[0] { + cmd = c + if err := c.Flags.Parse(args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(2) + } + break + } + } + + if cmd == nil { + fmt.Fprintf(os.Stderr, "%v: unknown subcommand: %q\n", cliName, args[0]) + fmt.Fprintf(os.Stderr, "Run '%v help' for usage.\n", cliName) + os.Exit(2) + } + os.Exit(cmd.Run(cmd.Flags.Args())) +} + +func getAllFlags() (flags []*flag.Flag) { + return getFlags(globalFlagset) +} + +func getFlags(flagset *flag.FlagSet) (flags []*flag.Flag) { + flags = make([]*flag.Flag, 0) + flagset.VisitAll(func(f *flag.Flag) { + flags = append(flags, f) + }) + return +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..ab03fda --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/coreos-inc/rkt/stage0" +) + +var ( + flagStage1Init string + flagStage1Rootfs string + flagVolumes volumeMap + cmdRun = &Command{ + Name: "run", + Summary: "Run image(s) in an application container in rocket", + Usage: "[--volume LABEL:SOURCE] IMAGE...", + Run: runRun, + } +) + +func init() { + cmdRun.Flags.StringVar(&flagStage1Init, "stage1-init", "./bin/init", "path to stage1 binary") + cmdRun.Flags.StringVar(&flagStage1Rootfs, "stage1-rootfs", "./stage1-rootfs.tar.gz", "path to stage1 rootfs tarball") + cmdRun.Flags.Var(&flagVolumes, "volume", "volumes to mount into the shared container environment") + flagVolumes = volumeMap{} +} + +func runRun(args []string) (exit int) { + if len(args) < 1 { + fmt.Fprintf(os.Stderr, "run: Must provide at least one image\n") + return 1 + } + cfg := stage0.Config{ + RktDir: globalFlags.Dir, + Debug: globalFlags.Debug, + Stage1Init: flagStage1Init, + Stage1Rootfs: flagStage1Rootfs, + Images: args, + Volumes: flagVolumes, + } + stage0.Run(cfg) // execs, never returns + return 1 +} + +// volumeMap implements the flag.Value interface to contain a set of mappings +// from mount label --> mount path +type volumeMap map[string]string + +func (vm *volumeMap) Set(s string) error { + elems := strings.Split(s, ":") + if len(elems) != 2 { + return errors.New("volume must be of form key:path") + } + key := elems[0] + if _, ok := (*vm)[key]; ok { + return fmt.Errorf("got multiple flags for volume %q", key) + } + (*vm)[key] = elems[1] + return nil +} + +func (vm *volumeMap) String() string { + var ss []string + for k, v := range *vm { + ss = append(ss, fmt.Sprintf("%s:%s", k, v)) + } + return strings.Join(ss, ",") +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..a8e881f --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + + "github.com/coreos-inc/rkt/rkt" +) + +var cmdVersion = &Command{ + Name: "version", + Description: "Print the version and exit", + Summary: "Print the version and exit", + Run: runVersion, +} + +func runVersion(args []string) (exit int) { + fmt.Printf("rkt version %s\n", rkt.Version) + return +} diff --git a/stage0/rkt.go b/stage0/run.go similarity index 77% rename from stage0/rkt.go rename to stage0/run.go index f4b34a2..9dd45fd 100644 --- a/stage0/rkt.go +++ b/stage0/run.go @@ -1,4 +1,4 @@ -package main +package stage0 // // Rocket is a reference implementation of the app container specification. @@ -29,15 +29,12 @@ import ( "compress/gzip" "crypto/sha256" "encoding/json" - "errors" - "flag" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" - "strings" "syscall" // WARNING: here be dragons @@ -48,42 +45,27 @@ import ( "github.com/coreos-inc/rkt/rkt" ) -var ( - fs = flag.NewFlagSet("rkt", flag.ExitOnError) - - flagDebug bool - flagDir string - flagStage1Init string - flagStage1Rfs string - flagVolumes volumeMap -) +type Config struct { + RktDir string + Stage1Init string + Stage1Rootfs string + Debug bool + Images []string + Volumes map[string]string +} func init() { log.SetOutput(ioutil.Discard) - fs.BoolVar(&flagDebug, "debug", false, "output debugging log information") - fs.StringVar(&flagDir, "dir", "", "directory in which to create container filesystem") - fs.StringVar(&flagStage1Init, "stage1-init", "./bin/init", "path to stage1 binary") - fs.StringVar(&flagStage1Rfs, "stage1-rootfs", "./stage1-rootfs.tar.gz", "path to stage1 rootfs tarball") - fs.Var(&flagVolumes, "volume", "volumes to mount into the shared container environment") - flagVolumes = volumeMap{} } -func main() { - fs.Parse(os.Args[1:]) - if flagDebug { +func Run(cfg Config) { + if cfg.Debug { log.SetOutput(os.Stderr) } - args := fs.Args() - if len(args) < 2 || args[0] != "run" { - fmt.Fprintf(os.Stderr, "usage: rkt run [OPTION]... IMAGE...\n") - os.Exit(0) - } - images := args[1:] - dir := flagDir - if dir == "" { - log.Printf("-dir unset - using temporary directory") + if cfg.RktDir == "" { + log.Printf("rktDir unset - using temporary directory") var err error - dir, err = ioutil.TempDir("", "rkt") + cfg.RktDir, err = ioutil.TempDir("", "rkt") if err != nil { log.Fatalf("error creating temporary directory: %v", err) } @@ -95,13 +77,16 @@ func main() { log.Fatalf("error creating UID: %v", err) } + // Create a directory for this container + dir := filepath.Join(cfg.RktDir, cuid.String()) + // - Creating a filesystem for the container if err := os.MkdirAll(dir, 0700); err != nil { log.Fatalf("error creating directory: %v", err) } log.Printf("Writing stage1 rootfs") - fh, err := os.Open(flagStage1Rfs) + fh, err := os.Open(cfg.Stage1Rootfs) if err != nil { log.Fatalf("error opening stage1 rootfs: %v", err) } @@ -118,7 +103,7 @@ func main() { } log.Printf("Writing stage1 init") - in, err := os.Open(flagStage1Init) + in, err := os.Open(cfg.Stage1Init) if err != nil { log.Fatalf("error loading stage1 binary: %v", err) } @@ -156,7 +141,7 @@ func main() { // TODO(jonboulle): clarify imagehash<->appname. Right now we have to // unpack the entire TAF to access the manifest which contains the appname. - for _, img := range images { + for _, img := range cfg.Images { h, err := types.NewHash(img) if err != nil { log.Fatalf("bad hash given: %v", err) @@ -224,7 +209,7 @@ func main() { } var sVols []types.Volume - for key, path := range flagVolumes { + for key, path := range cfg.Volumes { v := types.Volume{ Kind: "host", Source: path, @@ -255,8 +240,8 @@ func main() { log.Printf("Execing stage1/init") init := "stage1/init" - args = []string{init} - if flagDebug { + args := []string{init} + if cfg.Debug { args = append(args, "debug") } if err := syscall.Exec(init, args, os.Environ()); err != nil { @@ -269,28 +254,3 @@ func main() { func genUID() string { return "6733C088-A507-4694-AABF-EDBE4FC5266F" } - -// volumeMap implements the flag.Value interface to contain a set of mappings -// from mount label --> mount path -type volumeMap map[string]string - -func (vm *volumeMap) Set(s string) error { - elems := strings.Split(s, ":") - if len(elems) != 2 { - return errors.New("volume must be of form key:path") - } - key := elems[0] - if _, ok := (*vm)[key]; ok { - return fmt.Errorf("got multiple flags for volume %q", key) - } - (*vm)[key] = elems[1] - return nil -} - -func (vm *volumeMap) String() string { - var ss []string - for k, v := range *vm { - ss = append(ss, fmt.Sprintf("%s:%s", k, v)) - } - return strings.Join(ss, ",") -}