diff --git a/cli/cli.go b/cli/cli.go index c16d409c..87d06326 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -21,379 +21,127 @@ package cli import ( + "context" "fmt" - "log" - "sort" + "os" cliUtil "github.com/purpleidea/mgmt/cli/util" - "github.com/purpleidea/mgmt/gapi" - _ "github.com/purpleidea/mgmt/lang" // import so the GAPI registers - _ "github.com/purpleidea/mgmt/yamlgraph" + "github.com/purpleidea/mgmt/util/errwrap" - "github.com/urfave/cli/v2" + "github.com/alexflint/go-arg" ) // CLI is the entry point for using mgmt normally from the CLI. -func CLI(data *cliUtil.Data) error { +func CLI(ctx context.Context, data *cliUtil.Data) error { // test for sanity if data == nil { return fmt.Errorf("this CLI was not run correctly") } if data.Program == "" || data.Version == "" { - return fmt.Errorf("program was not compiled correctly, see Makefile") + return fmt.Errorf("program was not compiled correctly") } if data.Copying == "" { return fmt.Errorf("program copyrights were removed, can't run") } - // All of these flags can be accessed in your GAPI implementation with - // the `c.Lineage()[1].Type` and `c.Lineage()[1].IsSet` functions. Their - // own flags can be accessed with `c.Type` and `c.IsSet` directly. - runFlags := []cli.Flag{ - // common flags which all can use + args := Args{} + args.version = data.Version // copy this in + args.description = data.Tagline - // useful for testing multiple instances on same machine - &cli.StringFlag{ - Name: "hostname", - Value: "", - Usage: "hostname to use", - }, - - &cli.StringFlag{ - Name: "prefix", - Usage: "specify a path to the working prefix directory", - EnvVars: []string{"MGMT_PREFIX"}, - }, - &cli.BoolFlag{ - Name: "tmp-prefix", - Usage: "request a pseudo-random, temporary prefix to be used", - }, - &cli.BoolFlag{ - Name: "allow-tmp-prefix", - Usage: "allow creation of a new temporary prefix if main prefix is unavailable", - }, - - &cli.BoolFlag{ - Name: "no-watch", - Usage: "do not update graph under any switch events", - }, - &cli.BoolFlag{ - Name: "no-stream-watch", - Usage: "do not update graph on stream switch events", - }, - &cli.BoolFlag{ - Name: "no-deploy-watch", - Usage: "do not change deploys after an initial deploy", - }, - - &cli.BoolFlag{ - Name: "noop", - Usage: "globally force all resources into no-op mode", - }, - &cli.IntFlag{ - Name: "sema", - Value: -1, - Usage: "globally add a semaphore to all resources with this lock count", - }, - &cli.StringFlag{ - Name: "graphviz, g", - Value: "", - Usage: "output file for graphviz data", - }, - &cli.StringFlag{ - Name: "graphviz-filter, gf", - Value: "", - Usage: "graphviz filter to use", - }, - &cli.Int64Flag{ - Name: "converged-timeout, t", - Value: -1, - Usage: "after approximately this many seconds without activity, we're considered to be in a converged state", - EnvVars: []string{"MGMT_CONVERGED_TIMEOUT"}, - }, - &cli.BoolFlag{ - Name: "converged-timeout-no-exit", - Usage: "don't exit on converged-timeout", - }, - &cli.StringFlag{ - Name: "converged-status-file", - Value: "", - Usage: "file to append the current converged state to, mostly used for testing", - }, - &cli.IntFlag{ - Name: "max-runtime", - Value: 0, - Usage: "exit after a maximum of approximately this many seconds", - EnvVars: []string{"MGMT_MAX_RUNTIME"}, - }, - - // if empty, it will startup a new server - &cli.StringSliceFlag{ - Name: "seeds, s", - Value: &cli.StringSlice{}, // empty slice - Usage: "default etc client endpoint", - EnvVars: []string{"MGMT_SEEDS"}, - }, - // port 2379 and 4001 are common - &cli.StringSliceFlag{ - Name: "client-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for client traffic", - EnvVars: []string{"MGMT_CLIENT_URLS"}, - }, - // port 2380 and 7001 are common - &cli.StringSliceFlag{ - Name: "server-urls, peer-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for server (peer) traffic", - EnvVars: []string{"MGMT_SERVER_URLS"}, - }, - // port 2379 and 4001 are common - &cli.StringSliceFlag{ - Name: "advertise-client-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for client traffic", - EnvVars: []string{"MGMT_ADVERTISE_CLIENT_URLS"}, - }, - // port 2380 and 7001 are common - &cli.StringSliceFlag{ - Name: "advertise-server-urls, advertise-peer-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for server (peer) traffic", - EnvVars: []string{"MGMT_ADVERTISE_SERVER_URLS"}, - }, - &cli.IntFlag{ - Name: "ideal-cluster-size", - Value: -1, - Usage: "ideal number of server peers in cluster; only read by initial server", - EnvVars: []string{"MGMT_IDEAL_CLUSTER_SIZE"}, - }, - &cli.BoolFlag{ - Name: "no-server", - Usage: "do not start embedded etcd server (do not promote from client to peer)", - }, - &cli.BoolFlag{ - Name: "no-network", - Usage: "run single node instance without clustering or opening tcp ports to the outside", - EnvVars: []string{"MGMT_NO_NETWORK"}, - }, - &cli.BoolFlag{ - Name: "no-pgp", - Usage: "don't create pgp keys", - }, - &cli.StringFlag{ - Name: "pgp-key-path", - Value: "", - Usage: "path for instance key pair", - }, - &cli.StringFlag{ - Name: "pgp-identity", - Value: "", - Usage: "default identity used for generation", - }, - &cli.BoolFlag{ - Name: "prometheus", - Usage: "start a prometheus instance", - }, - &cli.StringFlag{ - Name: "prometheus-listen", - Value: "", - Usage: "specify prometheus instance binding", - }, + config := arg.Config{ + Program: data.Program, } - deployFlags := []cli.Flag{ - // common flags which all can use - &cli.StringSliceFlag{ - Name: "seeds, s", - Value: &cli.StringSlice{}, // empty slice - Usage: "default etc client endpoint", - EnvVars: []string{"MGMT_SEEDS"}, - }, - &cli.BoolFlag{ - Name: "noop", - Usage: "globally force all resources into no-op mode", - }, - &cli.IntFlag{ - Name: "sema", - Value: -1, - Usage: "globally add a semaphore to all resources with this lock count", - }, - - &cli.BoolFlag{ - Name: "no-git", - Usage: "don't look at git commit id for safe deploys", - }, - &cli.BoolFlag{ - Name: "force", - Usage: "force a new deploy, even if the safety chain would break", - }, + parser, err := arg.NewParser(config, &args) + if err != nil { + // programming error + return errwrap.Wrapf(err, "cli config error") } - getFlags := []cli.Flag{ - // common flags which all can use - &cli.BoolFlag{ - Name: "noop", - Usage: "simulate the download (can't recurse)", - }, - &cli.IntFlag{ - Name: "sema", - Value: -1, // maximum parallelism - Usage: "globally add a semaphore to downloads with this lock count", - }, - &cli.BoolFlag{ - Name: "update", - Usage: "update all dependencies to the latest versions", - }, + err = parser.Parse(data.Args[1:]) // XXX: args[0] needs to be dropped + if err == arg.ErrHelp { + parser.WriteHelp(os.Stdout) + return nil + } + if err == arg.ErrVersion { + fmt.Printf("%s\n", data.Version) // byon: bring your own newline + return nil + } + if err != nil { + //parser.WriteHelp(os.Stdout) // TODO: is doing this helpful? + return cliUtil.CliParseError(err) // consistent errors } - subCommandsRun := []*cli.Command{} // run sub commands - subCommandsDeploy := []*cli.Command{} // deploy sub commands - subCommandsGet := []*cli.Command{} // get (download) sub commands - - names := []string{} - for name := range gapi.RegisteredGAPIs { - names = append(names, name) - } - sort.Strings(names) // ensure deterministic order when parsing - for _, x := range names { - name := x // create a copy in this scope - fn := gapi.RegisteredGAPIs[name] - gapiObj := fn() - - commandRun := &cli.Command{ - Name: name, - Usage: fmt.Sprintf("run using the `%s` frontend", name), - Action: func(c *cli.Context) error { - if err := run(c, name, gapiObj); err != nil { - log.Printf("run: error: %v", err) - //return cli.NewExitError(err.Error(), 1) // TODO: ? - return cli.NewExitError("", 1) - } - return nil - }, - Flags: gapiObj.CliFlags(gapi.CommandRun), - } - subCommandsRun = append(subCommandsRun, commandRun) - - commandDeploy := &cli.Command{ - Name: name, - Usage: fmt.Sprintf("deploy using the `%s` frontend", name), - Action: func(c *cli.Context) error { - if err := deploy(c, name, gapiObj); err != nil { - log.Printf("deploy: error: %v", err) - //return cli.NewExitError(err.Error(), 1) // TODO: ? - return cli.NewExitError("", 1) - } - return nil - }, - Flags: gapiObj.CliFlags(gapi.CommandDeploy), - } - subCommandsDeploy = append(subCommandsDeploy, commandDeploy) - - if _, ok := gapiObj.(gapi.GettableGAPI); ok { - commandGet := &cli.Command{ - Name: name, - Usage: fmt.Sprintf("get (download) using the `%s` frontend", name), - Action: func(c *cli.Context) error { - if err := get(c, name, gapiObj); err != nil { - log.Printf("get: error: %v", err) - //return cli.NewExitError(err.Error(), 1) // TODO: ? - return cli.NewExitError("", 1) - } - return nil - }, - Flags: gapiObj.CliFlags(gapi.CommandGet), - } - subCommandsGet = append(subCommandsGet, commandGet) - } - } - - app := cli.NewApp() - app.Name = data.Program // App.name and App.version pass these values through - app.Version = data.Version - app.Usage = "next generation config management" - app.Metadata = map[string]interface{}{ // additional flags - "flags": data.Flags, - } - - // if no app.Command is specified - app.Action = func(c *cli.Context) error { - // print the license - if c.Bool("license") { - fmt.Printf("%s", data.Copying) - return nil - } - - // print help if no flags are set - cli.ShowAppHelp(c) + // display the license + if args.License { + fmt.Printf("%s", data.Copying) // file comes with a trailing nl return nil } - // global flags - app.Flags = []cli.Flag{ - &cli.BoolFlag{ - Name: "license", - Usage: "prints the software license", - }, + if ok, err := args.Run(ctx, data); err != nil { + return err + } else if ok { // did we activate one of the commands? + return nil } - app.Commands = []*cli.Command{ - //{ - // Name: gapi.CommandTODO, - // Aliases: []string{"TODO"}, - // Usage: "TODO", - // Action: TODO, - // Flags: TODOFlags, - //}, - } + // print help if no subcommands are set + parser.WriteHelp(os.Stdout) - // run always requires a frontend to start the engine, but if you don't - // want a graph, you can use the `empty` frontend. The engine backend is - // agnostic to which frontend is running, in fact, you can deploy with - // multiple different frontends, one after another, on the same engine. - if len(subCommandsRun) > 0 { - commandRun := &cli.Command{ - Name: gapi.CommandRun, - Aliases: []string{"r"}, - Usage: "Run code on this machine", - Subcommands: subCommandsRun, - Flags: runFlags, - } - app.Commands = append(app.Commands, commandRun) - } - - if len(subCommandsDeploy) > 0 { - commandDeploy := &cli.Command{ - Name: gapi.CommandDeploy, - Aliases: []string{"d"}, - Usage: "Deploy code into the cluster", - Subcommands: subCommandsDeploy, - Flags: deployFlags, - } - app.Commands = append(app.Commands, commandDeploy) - } - - if len(subCommandsGet) > 0 { - commandGet := &cli.Command{ - Name: gapi.CommandGet, - Aliases: []string{"g"}, - Usage: "Download code from the internet", - Subcommands: subCommandsGet, - Flags: getFlags, - } - app.Commands = append(app.Commands, commandGet) - } - - commandEtcd := &cli.Command{ - Name: "etcd", - //Aliases: []string{"e"}, - Usage: "Run standalone etcd", - Action: func(*cli.Context) error { - // this never runs, it gets preempted in the real main() - return nil - }, - } - app.Commands = append(app.Commands, commandEtcd) - - app.EnableBashCompletion = true - return app.Run(data.Args) + return nil } + +// Args is the CLI parsing structure and type of the parsed result. This +// particular struct is the top-most one. +type Args struct { + // XXX: We cannot have both subcommands and a positional argument. + // XXX: I think it's a bug of this library that it can't handle argv[0]. + //Argv0 string `arg:"positional"` + + License bool `arg:"--license" help:"display the license and exit"` + + RunCmd *RunArgs `arg:"subcommand:run" help:"run code on this machine"` + + DeployCmd *DeployArgs `arg:"subcommand:deploy" help:"deploy code into a cluster"` + + // This never runs, it gets preempted in the real main() function. + // XXX: Can we do it nicely with the new arg parser? can it ignore all args? + EtcdCmd *EtcdArgs `arg:"subcommand:etcd" help:"run standalone etcd"` + + // version is a private handle for our version string. + version string `arg:"-"` // ignored from parsing + + // description is a private handle for our description string. + description string `arg:"-"` // ignored from parsing +} + +// Version returns the version string. Implementing this signature is part of +// the API for the cli library. +func (obj *Args) Version() string { + return obj.version +} + +// Description returns a description string. Implementing this signature is part +// of the API for the cli library. +func (obj *Args) Description() string { + return obj.description +} + +// Run executes the correct subcommand. It errors if there's ever an error. It +// returns true if we did activate one of the subcommands. It returns false if +// we did not. This information is used so that the top-level parser can return +// usage or help information if no subcommand activates. +func (obj *Args) Run(ctx context.Context, data *cliUtil.Data) (bool, error) { + if cmd := obj.RunCmd; cmd != nil { + return cmd.Run(ctx, data) + } + + if cmd := obj.DeployCmd; cmd != nil { + return cmd.Run(ctx, data) + } + + // NOTE: we could return true, fmt.Errorf("...") if more than one did + return false, nil // nobody activated +} + +// EtcdArgs is the CLI parsing structure and type of the parsed result. This +// particular one is empty because the `etcd` subcommand is preempted in the +// real main() function. +type EtcdArgs struct{} diff --git a/cli/deploy.go b/cli/deploy.go index cbf66524..6db7c126 100644 --- a/cli/deploy.go +++ b/cli/deploy.go @@ -22,58 +22,103 @@ import ( "fmt" "log" "os" + "os/signal" cliUtil "github.com/purpleidea/mgmt/cli/util" "github.com/purpleidea/mgmt/etcd/client" "github.com/purpleidea/mgmt/etcd/deployer" etcdfs "github.com/purpleidea/mgmt/etcd/fs" "github.com/purpleidea/mgmt/gapi" + emptyGAPI "github.com/purpleidea/mgmt/gapi/empty" + langGAPI "github.com/purpleidea/mgmt/lang/gapi" "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/util/errwrap" + yamlGAPI "github.com/purpleidea/mgmt/yamlgraph" "github.com/pborman/uuid" - "github.com/urfave/cli/v2" git "gopkg.in/src-d/go-git.v4" ) -// deploy is the cli target to manage deploys to our cluster. -// TODO: add a timeout and/or cancel signal to replace context.TODO() -func deploy(c *cli.Context, name string, gapiObj gapi.GAPI) error { - cliContext := c.Lineage()[1] - if cliContext == nil { - return fmt.Errorf("could not get cli context") +// DeployArgs is the CLI parsing structure and type of the parsed result. This +// particular one contains all the common flags for the `deploy` subcommand +// which all frontends can use. +type DeployArgs struct { + Seeds []string `arg:"--seeds,env:MGMT_SEEDS" help:"default etc client endpoint"` + Noop bool `arg:"--noop" help:"globally force all resources into no-op mode"` + Sema int `arg:"--sema" default:"-1" help:"globally add a semaphore to all resources with this lock count"` + NoGit bool `arg:"--no-git" help:"don't look at git commit id for safe deploys"` + Force bool `arg:"--force" help:"force a new deploy, even if the safety chain would break"` + + DeployEmpty *emptyGAPI.Args `arg:"subcommand:empty" help:"deploy empty payload"` + DeployLang *langGAPI.Args `arg:"subcommand:lang" help:"deploy lang (mcl) payload"` + DeployYaml *yamlGAPI.Args `arg:"subcommand:yaml" help:"deploy yaml graph payload"` +} + +// Run executes the correct subcommand. It errors if there's ever an error. It +// returns true if we did activate one of the subcommands. It returns false if +// we did not. This information is used so that the top-level parser can return +// usage or help information if no subcommand activates. This particular Run is +// the run for the main `deploy` subcommand. This always requires a frontend to +// deploy to the cluster, but if you don't want a graph, you can use the `empty` +// frontend. The engine backend is agnostic to which frontend is deployed, in +// fact, you can deploy with multiple different frontends, one after another, on +// the same engine. +func (obj *DeployArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error) { + var name string + var args interface{} + if cmd := obj.DeployEmpty; cmd != nil { + name = emptyGAPI.Name + args = cmd + } + if cmd := obj.DeployLang; cmd != nil { + name = langGAPI.Name + args = cmd + } + if cmd := obj.DeployYaml; cmd != nil { + name = yamlGAPI.Name + args = cmd } - program, version := cliUtil.SafeProgram(c.App.Name), c.App.Version - var flags cliUtil.Flags - var debug bool - if val, exists := c.App.Metadata["flags"]; exists { - if f, ok := val.(cliUtil.Flags); ok { - flags = f - debug = flags.Debug + // XXX: workaround https://github.com/alexflint/go-arg/issues/239 + if l := len(obj.Seeds); name == "" && l > 1 { + elem := obj.Seeds[l-2] // second to last element + if elem == emptyGAPI.Name || elem == langGAPI.Name || elem == yamlGAPI.Name { + return false, cliUtil.CliParseError(cliUtil.MissingEquals) // consistent errors } } + + fn, exists := gapi.RegisteredGAPIs[name] + if !exists { + return false, nil // did not activate + } + gapiObj := fn() + + program, version := data.Program, data.Version Logf := func(format string, v ...interface{}) { log.Printf("deploy: "+format, v...) } - cliUtil.Hello(program, version, flags) // say hello! + // TODO: consider adding a timeout based on an args.Timeout flag ? + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() + + cliUtil.Hello(program, version, data.Flags) // say hello! defer Logf("goodbye!") var hash, pHash string - if !cliContext.Bool("no-git") { + if !obj.NoGit { wd, err := os.Getwd() if err != nil { - return errwrap.Wrapf(err, "could not get current working directory") + return false, errwrap.Wrapf(err, "could not get current working directory") } repo, err := git.PlainOpen(wd) if err != nil { - return errwrap.Wrapf(err, "could not open git repo") + return false, errwrap.Wrapf(err, "could not open git repo") } head, err := repo.Head() if err != nil { - return errwrap.Wrapf(err, "could not read git HEAD") + return false, errwrap.Wrapf(err, "could not read git HEAD") } hash = head.Hash().String() // current commit id @@ -84,32 +129,32 @@ func deploy(c *cli.Context, name string, gapiObj gapi.GAPI) error { } commits, err := repo.Log(lo) if err != nil { - return errwrap.Wrapf(err, "could not read git log") + return false, errwrap.Wrapf(err, "could not read git log") } if _, err := commits.Next(); err != nil { // skip over HEAD - return errwrap.Wrapf(err, "could not read HEAD in git log") // weird! + return false, errwrap.Wrapf(err, "could not read HEAD in git log") // weird! } commit, err := commits.Next() if err == nil { // errors are okay, we might be empty pHash = commit.Hash.String() // previous commit id } Logf("previous deploy hash: %s", pHash) - if cliContext.Bool("force") { + if obj.Force { pHash = "" // don't check this :( } if hash == "" { - return errwrap.Wrapf(err, "could not get git deploy hash") + return false, errwrap.Wrapf(err, "could not get git deploy hash") } } uniqueid := uuid.New() // panic's if it can't generate one :P etcdClient := client.NewClientFromSeedsNamespace( - cliContext.StringSlice("seeds"), // endpoints + obj.Seeds, // endpoints lib.NS, ) if err := etcdClient.Init(); err != nil { - return errwrap.Wrapf(err, "client Init failed") + return false, errwrap.Wrapf(err, "client Init failed") } defer func() { err := errwrap.Wrapf(etcdClient.Close(), "client Close failed") @@ -121,13 +166,13 @@ func deploy(c *cli.Context, name string, gapiObj gapi.GAPI) error { simpleDeploy := &deployer.SimpleDeploy{ Client: etcdClient, - Debug: debug, + Debug: data.Flags.Debug, Logf: func(format string, v ...interface{}) { Logf("deploy: "+format, v...) }, } if err := simpleDeploy.Init(); err != nil { - return errwrap.Wrapf(err, "deploy Init failed") + return false, errwrap.Wrapf(err, "deploy Init failed") } defer func() { err := errwrap.Wrapf(simpleDeploy.Close(), "deploy Close failed") @@ -138,9 +183,9 @@ func deploy(c *cli.Context, name string, gapiObj gapi.GAPI) error { }() // get max id (from all the previous deploys) - max, err := simpleDeploy.GetMaxDeployID(context.TODO()) + max, err := simpleDeploy.GetMaxDeployID(ctx) if err != nil { - return errwrap.Wrapf(err, "error getting max deploy id") + return false, errwrap.Wrapf(err, "error getting max deploy id") } // find the latest id var id = max + 1 // next id @@ -152,44 +197,49 @@ func deploy(c *cli.Context, name string, gapiObj gapi.GAPI) error { Metadata: lib.MetadataPrefix + fmt.Sprintf("/deploy/%d-%s", id, uniqueid), DataPrefix: lib.StoragePrefix, - Debug: debug, + Debug: data.Flags.Debug, Logf: func(format string, v ...interface{}) { Logf("fs: "+format, v...) }, } - cliInfo := &gapi.CliInfo{ - CliContext: c, // don't pass in the parent context + info := &gapi.Info{ + Args: args, + Flags: &gapi.Flags{ + Noop: obj.Noop, + Sema: obj.Sema, + //Update: obj.Update, + }, Fs: etcdFs, - Debug: debug, + Debug: data.Flags.Debug, Logf: func(format string, v ...interface{}) { // TODO: is this a sane prefix to use here? log.Printf("cli: "+format, v...) }, } - deploy, err := gapiObj.Cli(cliInfo) + deploy, err := gapiObj.Cli(info) if err != nil { - return errwrap.Wrapf(err, "cli parse error") + return false, cliUtil.CliParseError(err) // consistent errors } if deploy == nil { // not used - return fmt.Errorf("not enough information specified") + return false, fmt.Errorf("not enough information specified") } // redundant - deploy.Noop = cliContext.Bool("noop") - deploy.Sema = cliContext.Int("sema") + deploy.Noop = obj.Noop + deploy.Sema = obj.Sema str, err := deploy.ToB64() if err != nil { - return errwrap.Wrapf(err, "encoding error") + return false, errwrap.Wrapf(err, "encoding error") } // this nominally checks the previous git hash matches our expectation - if err := simpleDeploy.AddDeploy(context.TODO(), id, hash, pHash, &str); err != nil { - return errwrap.Wrapf(err, "could not create deploy id `%d`", id) + if err := simpleDeploy.AddDeploy(ctx, id, hash, pHash, &str); err != nil { + return false, errwrap.Wrapf(err, "could not create deploy id `%d`", id) } Logf("success, id: %d", id) - return nil + return true, nil } diff --git a/cli/get.go b/cli/get.go deleted file mode 100644 index f9eea399..00000000 --- a/cli/get.go +++ /dev/null @@ -1,74 +0,0 @@ -// Mgmt -// Copyright (C) 2013-2024+ James Shubin and the project contributors -// Written by James Shubin and the project contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package cli - -import ( - "fmt" - "log" - - cliUtil "github.com/purpleidea/mgmt/cli/util" - "github.com/purpleidea/mgmt/gapi" - - "github.com/urfave/cli/v2" -) - -// get is the cli target to run code/import downloads. -func get(c *cli.Context, name string, gapiObj gapi.GAPI) error { - cliContext := c.Lineage()[1] - if cliContext == nil { - return fmt.Errorf("could not get cli context") - } - - program, version := cliUtil.SafeProgram(c.App.Name), c.App.Version - var flags cliUtil.Flags - var debug bool - if val, exists := c.App.Metadata["flags"]; exists { - if f, ok := val.(cliUtil.Flags); ok { - flags = f - debug = flags.Debug - } - } - cliUtil.Hello(program, version, flags) // say hello! - - gettable, ok := gapiObj.(gapi.GettableGAPI) - if !ok { - // this is a programming bug as this should not get called... - return fmt.Errorf("the `%s` GAPI does not implement: %s", name, gapi.CommandGet) - } - - getInfo := &gapi.GetInfo{ - CliContext: c, // don't pass in the parent context - - Noop: cliContext.Bool("noop"), - Sema: cliContext.Int("sema"), - Update: cliContext.Bool("update"), - - Debug: debug, - Logf: func(format string, v ...interface{}) { - // TODO: is this a sane prefix to use here? - log.Printf(name+": "+format, v...) - }, - } - - if err := gettable.Get(getInfo); err != nil { - return err // no need to errwrap here - } - - log.Printf("%s: success!", name) - return nil -} diff --git a/cli/run.go b/cli/run.go index 3caf0d26..3c872465 100644 --- a/cli/run.go +++ b/cli/run.go @@ -18,6 +18,7 @@ package cli import ( + "context" "fmt" "log" "os" @@ -27,50 +28,140 @@ import ( cliUtil "github.com/purpleidea/mgmt/cli/util" "github.com/purpleidea/mgmt/gapi" + emptyGAPI "github.com/purpleidea/mgmt/gapi/empty" + langGAPI "github.com/purpleidea/mgmt/lang/gapi" "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" + yamlGAPI "github.com/purpleidea/mgmt/yamlgraph" "github.com/spf13/afero" - "github.com/urfave/cli/v2" ) -// run is the main run target. -func run(c *cli.Context, name string, gapiObj gapi.GAPI) error { - cliContext := c.Lineage()[1] // these are the flags from `run` - if cliContext == nil { - return fmt.Errorf("could not get cli context") +// RunArgs is the CLI parsing structure and type of the parsed result. This +// particular one contains all the common flags for the `run` subcommand which +// all frontends can use. +type RunArgs struct { + + // useful for testing multiple instances on same machine + Hostname *string `arg:"--hostname" help:"hostname to use"` + Prefix *string `arg:"--prefix,env:MGMT_PREFIX" help:"specify a path to the working prefix directory"` + TmpPrefix bool `arg:"--tmp-prefix" help:"request a pseudo-random, temporary prefix to be used"` + AllowTmpPrefix bool `arg:"--allow-tmp-prefix" help:"allow creation of a new temporary prefix if main prefix is unavailable"` + + NoWatch bool `arg:"--no-watch" help:"do not update graph under any switch events"` + NoStreamWatch bool `arg:"--no-stream-watch" help:"do not update graph on stream switch events"` + NoDeployWatch bool `arg:"--no-deploy-watch" help:"do not change deploys after an initial deploy"` + + Noop bool `arg:"--noop" help:"globally force all resources into no-op mode"` + Sema int `arg:"--sema" default:"-1" help:"globally add a semaphore to downloads with this lock count"` + + Graphviz string `arg:"--graphviz" help:"output file for graphviz data"` + GraphvizFilter string `arg:"--graphviz-filter" help:"graphviz filter to use"` + ConvergedTimeout int `arg:"--converged-timeout,env:MGMT_CONVERGED_TIMEOUT" default:"-1" help:"after approximately this many seconds without activity, we're considered to be in a converged state"` + ConvergedTimeoutNoExit bool `arg:"--converged-timeout-no-exit" help:"don't exit on converged-timeout"` + ConvergedStatusFile string `arg:"--converged-status-file" help:"file to append the current converged state to, mostly used for testing"` + MaxRuntime uint `arg:"--max-runtime,env:MGMT_MAX_RUNTIME" help:"exit after a maximum of approximately this many seconds"` + + // if empty, it will startup a new server + Seeds []string `arg:"--seeds,env:MGMT_SEEDS" help:"default etc client endpoint"` + + // port 2379 and 4001 are common + ClientURLs []string `arg:"--client-urls,env:MGMT_CLIENT_URLS" help:"list of URLs to listen on for client traffic"` + + // port 2380 and 7001 are common + // etcd now uses --peer-urls + ServerURLs []string `arg:"--server-urls,env:MGMT_SERVER_URLS" help:"list of URLs to listen on for server (peer) traffic"` + + // port 2379 and 4001 are common + AdvertiseClientURLs []string `arg:"--advertise-client-urls,env:MGMT_ADVERTISE_CLIENT_URLS" help:"list of URLs to listen on for client traffic"` + + // port 2380 and 7001 are common + // etcd now uses --advertise-peer-urls + AdvertiseServerURLs []string `arg:"--advertise-server-urls,env:MGMT_ADVERTISE_SERVER_URLS" help:"list of URLs to listen on for server (peer) traffic"` + + IdealClusterSize int `arg:"--ideal-cluster-size,env:MGMT_IDEAL_CLUSTER_SIZE" default:"-1" help:"ideal number of server peers in cluster; only read by initial server"` + NoServer bool `arg:"--no-server" help:"do not start embedded etcd server (do not promote from client to peer)"` + NoNetwork bool `arg:"--no-network,env:MGMT_NO_NETWORK" help:"run single node instance without clustering or opening tcp ports to the outside"` + + NoPgp bool `arg:"--no-pgp" help:"don't create pgp keys"` + PgpKeyPath *string `arg:"--pgp-key-path" help:"path for instance key pair"` + PgpIdentity *string `arg:"--pgp-identity" help:"default identity used for generation"` + + Prometheus bool `arg:"--prometheus" help:"start a prometheus instance"` + PrometheusListen string `arg:"--prometheus-listen" help:"specify prometheus instance binding"` + + RunEmpty *emptyGAPI.Args `arg:"subcommand:empty" help:"run empty payload"` + RunLang *langGAPI.Args `arg:"subcommand:lang" help:"run lang (mcl) payload"` + RunYaml *yamlGAPI.Args `arg:"subcommand:yaml" help:"run yaml graph payload"` +} + +// Run executes the correct subcommand. It errors if there's ever an error. It +// returns true if we did activate one of the subcommands. It returns false if +// we did not. This information is used so that the top-level parser can return +// usage or help information if no subcommand activates. This particular Run is +// the run for the main `run` subcommand. This always requires a frontend to +// start the engine, but if you don't want a graph, you can use the `empty` +// frontend. The engine backend is agnostic to which frontend is running, in +// fact, you can deploy with multiple different frontends, one after another, on +// the same engine. +func (obj *RunArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error) { + var name string + var args interface{} + if cmd := obj.RunEmpty; cmd != nil { + name = emptyGAPI.Name + args = cmd } + if cmd := obj.RunLang; cmd != nil { + name = langGAPI.Name + args = cmd + } + if cmd := obj.RunYaml; cmd != nil { + name = yamlGAPI.Name + args = cmd + } + + // XXX: workaround https://github.com/alexflint/go-arg/issues/239 + lists := [][]string{ + obj.Seeds, + obj.ClientURLs, + obj.ServerURLs, + obj.AdvertiseClientURLs, + obj.AdvertiseServerURLs, + } + for _, list := range lists { + if l := len(list); name == "" && l > 1 { + elem := list[l-2] // second to last element + if elem == emptyGAPI.Name || elem == langGAPI.Name || elem == yamlGAPI.Name { + return false, cliUtil.CliParseError(cliUtil.MissingEquals) // consistent errors + } + } + } + + fn, exists := gapi.RegisteredGAPIs[name] + if !exists { + return false, nil // did not activate + } + gapiObj := fn() main := &lib.Main{} - main.Program, main.Version = cliUtil.SafeProgram(c.App.Name), c.App.Version - var flags cliUtil.Flags - if val, exists := c.App.Metadata["flags"]; exists { - if f, ok := val.(cliUtil.Flags); ok { - flags = f - main.Flags = lib.Flags{ - Debug: f.Debug, - Verbose: f.Verbose, - } - } + main.Program, main.Version = data.Program, data.Version + main.Flags = lib.Flags{ + Debug: data.Flags.Debug, + Verbose: data.Flags.Verbose, } Logf := func(format string, v ...interface{}) { log.Printf("main: "+format, v...) } - cliUtil.Hello(main.Program, main.Version, flags) // say hello! + cliUtil.Hello(main.Program, main.Version, data.Flags) // say hello! defer Logf("goodbye!") - if h := cliContext.String("hostname"); cliContext.IsSet("hostname") && h != "" { - main.Hostname = &h - } - - if s := cliContext.String("prefix"); cliContext.IsSet("prefix") && s != "" { - main.Prefix = &s - } - main.TmpPrefix = cliContext.Bool("tmp-prefix") - main.AllowTmpPrefix = cliContext.Bool("allow-tmp-prefix") + main.Hostname = obj.Hostname + main.Prefix = obj.Prefix + main.TmpPrefix = obj.TmpPrefix + main.AllowTmpPrefix = obj.AllowTmpPrefix // create a memory backed temporary filesystem for storing runtime data mmFs := afero.NewMemMapFs() @@ -78,8 +169,14 @@ func run(c *cli.Context, name string, gapiObj gapi.GAPI) error { standaloneFs := &util.AferoFs{Afero: afs} main.DeployFs = standaloneFs - cliInfo := &gapi.CliInfo{ - CliContext: c, // don't pass in the parent context + info := &gapi.Info{ + Args: args, + Flags: &gapi.Flags{ + Hostname: obj.Hostname, + Noop: obj.Noop, + Sema: obj.Sema, + //Update: obj.Update, + }, Fs: standaloneFs, Debug: main.Flags.Debug, @@ -88,12 +185,16 @@ func run(c *cli.Context, name string, gapiObj gapi.GAPI) error { }, } - deploy, err := gapiObj.Cli(cliInfo) + deploy, err := gapiObj.Cli(info) if err != nil { - return errwrap.Wrapf(err, "cli parse error") + return false, cliUtil.CliParseError(err) // consistent errors } - if c.Bool("only-unify") && deploy == nil { - return nil // we end early + + if cmd := obj.RunLang; cmd != nil && cmd.OnlyUnify && deploy == nil { + return true, nil // we end early + } + if cmd := obj.RunLang; cmd != nil && cmd.OnlyDownload && deploy == nil { + return true, nil // we end early } main.Deploy = deploy if main.Deploy == nil { @@ -102,47 +203,41 @@ func run(c *cli.Context, name string, gapiObj gapi.GAPI) error { log.Printf("main: no frontend selected (no GAPI activated)") } - main.NoWatch = cliContext.Bool("no-watch") - main.NoStreamWatch = cliContext.Bool("no-stream-watch") - main.NoDeployWatch = cliContext.Bool("no-deploy-watch") + main.NoWatch = obj.NoWatch + main.NoStreamWatch = obj.NoStreamWatch + main.NoDeployWatch = obj.NoDeployWatch - main.Noop = cliContext.Bool("noop") - main.Sema = cliContext.Int("sema") - main.Graphviz = cliContext.String("graphviz") - main.GraphvizFilter = cliContext.String("graphviz-filter") - main.ConvergedTimeout = cliContext.Int64("converged-timeout") - main.ConvergedTimeoutNoExit = cliContext.Bool("converged-timeout-no-exit") - main.ConvergedStatusFile = cliContext.String("converged-status-file") - main.MaxRuntime = uint(cliContext.Int("max-runtime")) + main.Noop = obj.Noop + main.Sema = obj.Sema + main.Graphviz = obj.Graphviz + main.GraphvizFilter = obj.GraphvizFilter + main.ConvergedTimeout = obj.ConvergedTimeout + main.ConvergedTimeoutNoExit = obj.ConvergedTimeoutNoExit + main.ConvergedStatusFile = obj.ConvergedStatusFile + main.MaxRuntime = obj.MaxRuntime - main.Seeds = cliContext.StringSlice("seeds") - main.ClientURLs = cliContext.StringSlice("client-urls") - main.ServerURLs = cliContext.StringSlice("server-urls") - main.AdvertiseClientURLs = cliContext.StringSlice("advertise-client-urls") - main.AdvertiseServerURLs = cliContext.StringSlice("advertise-server-urls") - main.IdealClusterSize = cliContext.Int("ideal-cluster-size") - main.NoServer = cliContext.Bool("no-server") - main.NoNetwork = cliContext.Bool("no-network") + main.Seeds = obj.Seeds + main.ClientURLs = obj.ClientURLs + main.ServerURLs = obj.ServerURLs + main.AdvertiseClientURLs = obj.AdvertiseClientURLs + main.AdvertiseServerURLs = obj.AdvertiseServerURLs + main.IdealClusterSize = obj.IdealClusterSize + main.NoServer = obj.NoServer + main.NoNetwork = obj.NoNetwork - main.NoPgp = cliContext.Bool("no-pgp") + main.NoPgp = obj.Prometheus + main.PgpKeyPath = obj.PgpKeyPath + main.PgpIdentity = obj.PgpIdentity - if kp := cliContext.String("pgp-key-path"); cliContext.IsSet("pgp-key-path") { - main.PgpKeyPath = &kp - } - - if us := cliContext.String("pgp-identity"); cliContext.IsSet("pgp-identity") { - main.PgpIdentity = &us - } - - main.Prometheus = cliContext.Bool("prometheus") - main.PrometheusListen = cliContext.String("prometheus-listen") + main.Prometheus = obj.Prometheus + main.PrometheusListen = obj.PrometheusListen if err := main.Validate(); err != nil { - return err + return false, err } if err := main.Init(); err != nil { - return err + return false, err } // install the exit signal handler @@ -200,10 +295,13 @@ func run(c *cli.Context, name string, gapiObj gapi.GAPI) error { log.Printf("main: Close: %+v", err) } if reterr == nil { - return err + return false, err } reterr = errwrap.Append(reterr, err) } - return reterr + if reterr != nil { + return false, reterr + } + return true, nil } diff --git a/cli/util/util.go b/cli/util/util.go index 225cd1ee..64ebfb05 100644 --- a/cli/util/util.go +++ b/cli/util/util.go @@ -20,9 +20,29 @@ package util import ( "strings" + + "github.com/purpleidea/mgmt/util/errwrap" ) +// Error is a constant error type that implements error. +type Error string + +// Error fulfills the error interface of this type. +func (e Error) Error() string { return string(e) } + +const ( + // MissingEquals means we probably hit the parsing bug. + // XXX: see: https://github.com/alexflint/go-arg/issues/239 + MissingEquals = Error("missing equals sign for list element") +) + +// CliParseError returns a consistent error if we have a CLI parsing issue. +func CliParseError(err error) error { + return errwrap.Wrapf(err, "cli parse error") +} + // Flags are some constant flags which are used throughout the program. +// TODO: Unify this with Debug and Logf ? type Flags struct { Debug bool // add additional log messages Verbose bool // add extra log message output @@ -33,6 +53,7 @@ type Data struct { Program string Version string Copying string + Tagline string Flags Flags Args []string // os.Args usually } diff --git a/converger/converger.go b/converger/converger.go index d24c725f..de98c78e 100644 --- a/converger/converger.go +++ b/converger/converger.go @@ -29,7 +29,7 @@ import ( ) // New builds a new converger coordinator. -func New(timeout int64) *Coordinator { +func New(timeout int) *Coordinator { return &Coordinator{ timeout: timeout, @@ -61,7 +61,7 @@ func New(timeout int64) *Coordinator { type Coordinator struct { // timeout must be zero (instant) or greater seconds to run. If it's -1 // then this is disabled, and we never run stateFns. - timeout int64 + timeout int // mutex is used for controlling access to status and lastid. mutex *sync.RWMutex @@ -365,7 +365,7 @@ func (obj *Coordinator) Status() map[*UID]bool { // Timeout returns the timeout in seconds that converger was created with. This // is useful to avoid passing in the timeout value separately when you're // already passing in the Coordinator struct. -func (obj *Coordinator) Timeout() int64 { +func (obj *Coordinator) Timeout() int { return obj.timeout } @@ -375,7 +375,7 @@ func (obj *Coordinator) Timeout() int64 { type UID struct { // timeout is a copy of the main timeout. It could eventually be used // for per-UID timeouts too. - timeout int64 + timeout int // isConverged stores the convergence state of this particular UID. isConverged bool diff --git a/docs/faq.md b/docs/faq.md index 6f710a89..9c81b758 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -346,6 +346,32 @@ print "hello" { Yes we know the compiler gives horrible error messages, and yes we would absolutely love your help improving this. +### The run and deploy commands don't parse correctly when used with `--seeds`. + +If you're running a command with `--seeds`, `--server-urls`, or `--client-urls`, +then make sure you are using an equals sign between the flag name and the value. +For example, if you were to run: + +``` +# wrong invocation! +mgmt deploy --no-git --seeds http://127.0.0.1:2379 lang code/test.mcl +``` + +Then the `--seeds` flag would interpret `lang` and `code/test.mcl` as additional +seeds. This flag as well as the other aforementioned ones all accept multiple +values. Use an equals sign to guarantee you enter the correct data, eg: + +``` +# better invocation! (note the equals sign) +mgmt deploy --no-git --seeds=http://127.0.0.1:2379 lang code/test.mcl +``` + +This is caused by a parsing peculiarity of the CLI library that we are using. +This is tracked upstream at: [https://github.com/alexflint/go-arg/issues/239](https://github.com/alexflint/go-arg/issues/239). +We have a workaround in place to mitigate it and attempt to show you a helpful +error message, but it's also documented here in the meantime. The error you will +see is: `cli parse error: missing equals sign for list element`. + ### The docs speaks of `--remote` but the CLI errors out? The `--remote` flag existed in an earlier version of mgmt. It was removed and diff --git a/etcd/etcd.go b/etcd/etcd.go index 0792084a..a81d924e 100644 --- a/etcd/etcd.go +++ b/etcd/etcd.go @@ -70,11 +70,11 @@ // Here is a simple way to test etcd clustering basics... // // ./mgmt run --tmp-prefix --no-pgp --hostname h1 empty -// ./mgmt run --tmp-prefix --no-pgp --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 empty -// ./mgmt run --tmp-prefix --no-pgp --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 empty +// ./mgmt run --tmp-prefix --no-pgp --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 empty +// ./mgmt run --tmp-prefix --no-pgp --hostname h3 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 empty // ETCDCTL_API=3 etcdctl --endpoints 127.0.0.1:2379 put /_mgmt/chooser/dynamicsize/idealclustersize 3 -// ./mgmt run --tmp-prefix --no-pgp --hostname h4 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2385 --server-urls http://127.0.0.1:2386 empty -// ./mgmt run --tmp-prefix --no-pgp --hostname h5 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2387 --server-urls http://127.0.0.1:2388 empty +// ./mgmt run --tmp-prefix --no-pgp --hostname h4 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2385 --server-urls=http://127.0.0.1:2386 empty +// ./mgmt run --tmp-prefix --no-pgp --hostname h5 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2387 --server-urls=http://127.0.0.1:2388 empty // ETCDCTL_API=3 etcdctl --endpoints 127.0.0.1:2379 member list // ETCDCTL_API=3 etcdctl --endpoints 127.0.0.1:2381 put /_mgmt/chooser/dynamicsize/idealclustersize 5 // ETCDCTL_API=3 etcdctl --endpoints 127.0.0.1:2381 member list diff --git a/examples/lang/exchange0.mcl b/examples/lang/exchange0.mcl index 9ce8e60c..b9ac1a9a 100644 --- a/examples/lang/exchange0.mcl +++ b/examples/lang/exchange0.mcl @@ -1,10 +1,10 @@ # run this example with these commands # watch -n 0.1 'tail *' # run this in /tmp/mgmt/ # time ./mgmt run --hostname h1 --tmp-prefix --no-pgp empty -# time ./mgmt run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp empty -# time ./mgmt run --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp empty -# time ./mgmt run --hostname h4 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2385 --server-urls http://127.0.0.1:2386 --tmp-prefix --no-pgp empty -# time ./mgmt deploy --no-git --seeds http://127.0.0.1:2379 lang examples/lang/exchange0.mcl +# time ./mgmt run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix --no-pgp empty +# time ./mgmt run --hostname h3 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 --tmp-prefix --no-pgp empty +# time ./mgmt run --hostname h4 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2385 --server-urls=http://127.0.0.1:2386 --tmp-prefix --no-pgp empty +# time ./mgmt deploy --no-git --seeds=http://127.0.0.1:2379 lang examples/lang/exchange0.mcl import "sys" import "world" diff --git a/examples/lib/exec-send-recv.go b/examples/lib/exec-send-recv.go index 5ebeb04b..b8892a8a 100644 --- a/examples/lib/exec-send-recv.go +++ b/examples/lib/exec-send-recv.go @@ -15,8 +15,6 @@ import ( "github.com/purpleidea/mgmt/gapi" mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" - - "github.com/urfave/cli/v2" ) // XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! @@ -46,31 +44,20 @@ func NewMyGAPI(data *gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } -// CliFlags returns a list of flags used by the passed in subcommand. -func (obj *MyGAPI) CliFlags(string) []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: obj.Name, - Value: "", - Usage: "run", - }, - } -} - -// Cli takes a cli.Context and some other info, and returns our GAPI. If there -// are any validation problems, you should return an error. -func (obj *MyGAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { - c := cliInfo.CliContext - //fs := cliInfo.Fs // copy files from local filesystem *into* this fs... - //debug := cliInfo.Debug +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. +func (obj *MyGAPI) Cli(info *gapi.CliInfo) (*gapi.Deploy, error) { + //fs := info.Fs // copy files from local filesystem *into* this fs... + //debug := info.Debug //logf := func(format string, v ...interface{}) { - // cliInfo.Logf(Name+": "+format, v...) + // info.Logf(Name+": "+format, v...) //} return &gapi.Deploy{ Name: obj.Name, - Noop: c.Bool("noop"), - Sema: c.Int("sema"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, GAPI: &MyGAPI{}, }, nil } diff --git a/examples/lib/libmgmt-subgraph0.go b/examples/lib/libmgmt-subgraph0.go index 5fc1a79c..269a5ea2 100644 --- a/examples/lib/libmgmt-subgraph0.go +++ b/examples/lib/libmgmt-subgraph0.go @@ -16,8 +16,6 @@ import ( mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/util/errwrap" - - "github.com/urfave/cli/v2" ) // XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! @@ -51,31 +49,20 @@ func NewMyGAPI(data *gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } -// CliFlags returns a list of flags used by the passed in subcommand. -func (obj *MyGAPI) CliFlags(string) []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: obj.Name, - Value: "", - Usage: "run", - }, - } -} - -// Cli takes a cli.Context and some other info, and returns our GAPI. If there -// are any validation problems, you should return an error. -func (obj *MyGAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { - c := cliInfo.CliContext - //fs := cliInfo.Fs // copy files from local filesystem *into* this fs... - //debug := cliInfo.Debug +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. +func (obj *MyGAPI) Cli(info *gapi.CliInfo) (*gapi.Deploy, error) { + //fs := info.Fs // copy files from local filesystem *into* this fs... + //debug := info.Debug //logf := func(format string, v ...interface{}) { - // cliInfo.Logf(Name+": "+format, v...) + // info.Logf(Name+": "+format, v...) //} return &gapi.Deploy{ Name: obj.Name, - Noop: c.Bool("noop"), - Sema: c.Int("sema"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, GAPI: &MyGAPI{}, }, nil } diff --git a/examples/lib/libmgmt-subgraph1.go b/examples/lib/libmgmt-subgraph1.go index b17b76fa..0c8ba008 100644 --- a/examples/lib/libmgmt-subgraph1.go +++ b/examples/lib/libmgmt-subgraph1.go @@ -15,8 +15,6 @@ import ( "github.com/purpleidea/mgmt/gapi" mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" - - "github.com/urfave/cli/v2" ) // XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! @@ -46,31 +44,20 @@ func NewMyGAPI(data *gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } -// CliFlags returns a list of flags used by the passed in subcommand. -func (obj *MyGAPI) CliFlags(string) []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: obj.Name, - Value: "", - Usage: "run", - }, - } -} - -// Cli takes a cli.Context and some other info, and returns our GAPI. If there -// are any validation problems, you should return an error. -func (obj *MyGAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { - c := cliInfo.CliContext - //fs := cliInfo.Fs // copy files from local filesystem *into* this fs... - //debug := cliInfo.Debug +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. +func (obj *MyGAPI) Cli(info *gapi.CliInfo) (*gapi.Deploy, error) { + //fs := info.Fs // copy files from local filesystem *into* this fs... + //debug := info.Debug //logf := func(format string, v ...interface{}) { - // cliInfo.Logf(Name+": "+format, v...) + // info.Logf(Name+": "+format, v...) //} return &gapi.Deploy{ Name: obj.Name, - Noop: c.Bool("noop"), - Sema: c.Int("sema"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, GAPI: &MyGAPI{}, }, nil } diff --git a/examples/lib/libmgmt2.go b/examples/lib/libmgmt2.go index d05d66c1..a8fde007 100644 --- a/examples/lib/libmgmt2.go +++ b/examples/lib/libmgmt2.go @@ -15,8 +15,6 @@ import ( "github.com/purpleidea/mgmt/gapi" mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" - - "github.com/urfave/cli/v2" ) // XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! @@ -48,31 +46,20 @@ func NewMyGAPI(data *gapi.Data, name string, interval uint, count uint) (*MyGAPI return obj, obj.Init(data) } -// CliFlags returns a list of flags used by the passed in subcommand. -func (obj *MyGAPI) CliFlags(string) []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: obj.Name, - Value: "", - Usage: "run", - }, - } -} - -// Cli takes a cli.Context and some other info, and returns our GAPI. If there -// are any validation problems, you should return an error. -func (obj *MyGAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { - c := cliInfo.CliContext - //fs := cliInfo.Fs // copy files from local filesystem *into* this fs... - //debug := cliInfo.Debug +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. +func (obj *MyGAPI) Cli(info *gapi.CliInfo) (*gapi.Deploy, error) { + //fs := info.Fs // copy files from local filesystem *into* this fs... + //debug := info.Debug //logf := func(format string, v ...interface{}) { - // cliInfo.Logf(Name+": "+format, v...) + // info.Logf(Name+": "+format, v...) //} return &gapi.Deploy{ Name: obj.Name, - Noop: c.Bool("noop"), - Sema: c.Int("sema"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, GAPI: &MyGAPI{}, }, nil } diff --git a/examples/lib/libmgmt3.go b/examples/lib/libmgmt3.go index 735d669f..1f5429cd 100644 --- a/examples/lib/libmgmt3.go +++ b/examples/lib/libmgmt3.go @@ -15,8 +15,6 @@ import ( "github.com/purpleidea/mgmt/gapi" mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" - - "github.com/urfave/cli/v2" ) // XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! @@ -46,31 +44,20 @@ func NewMyGAPI(data *gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } -// CliFlags returns a list of flags used by the passed in subcommand. -func (obj *MyGAPI) CliFlags(string) []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: obj.Name, - Value: "", - Usage: "run", - }, - } -} - -// Cli takes a cli.Context and some other info, and returns our GAPI. If there -// are any validation problems, you should return an error. -func (obj *MyGAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { - c := cliInfo.CliContext - //fs := cliInfo.Fs // copy files from local filesystem *into* this fs... - //debug := cliInfo.Debug +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. +func (obj *MyGAPI) Cli(info *gapi.CliInfo) (*gapi.Deploy, error) { + //fs := info.Fs // copy files from local filesystem *into* this fs... + //debug := info.Debug //logf := func(format string, v ...interface{}) { - // cliInfo.Logf(Name+": "+format, v...) + // info.Logf(Name+": "+format, v...) //} return &gapi.Deploy{ Name: obj.Name, - Noop: c.Bool("noop"), - Sema: c.Int("sema"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, GAPI: &MyGAPI{}, }, nil } diff --git a/gapi/empty/empty.go b/gapi/empty/empty.go index 2b852969..1ba2af4a 100644 --- a/gapi/empty/empty.go +++ b/gapi/empty/empty.go @@ -23,8 +23,6 @@ import ( "github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/pgraph" - - "github.com/urfave/cli/v2" ) const ( @@ -36,6 +34,9 @@ func init() { gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register } +// Args is the CLI parsing structure and type of the parsed result. +type Args struct{} + // GAPI implements the main lang GAPI interface. type GAPI struct { data *gapi.Data @@ -44,16 +45,10 @@ type GAPI struct { wg *sync.WaitGroup // sync group for tunnel go routines } -// CliFlags returns a list of flags used by the specified subcommand. -func (obj *GAPI) CliFlags(command string) []cli.Flag { - return []cli.Flag{} -} - -// Cli takes a cli.Context, and returns our GAPI if activated. All arguments -// should take the prefix of the registered name. On activation, if there are -// any validation problems, you should return an error. If this was not -// activated, then you should return a nil GAPI and a nil error. -func (obj *GAPI) Cli(*gapi.CliInfo) (*gapi.Deploy, error) { +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. +func (obj *GAPI) Cli(*gapi.Info) (*gapi.Deploy, error) { return &gapi.Deploy{ Name: Name, //Noop: false, diff --git a/gapi/gapi.go b/gapi/gapi.go index 251cf669..f8428ec1 100644 --- a/gapi/gapi.go +++ b/gapi/gapi.go @@ -25,20 +25,6 @@ import ( "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/engine/local" "github.com/purpleidea/mgmt/pgraph" - - "github.com/urfave/cli/v2" -) - -const ( - // CommandRun is the identifier for the "run" command. It is distinct - // from the other commands, because it can run with any front-end. - CommandRun = "run" - - // CommandDeploy is the identifier for the "deploy" command. - CommandDeploy = "deploy" - - // CommandGet is the identifier for the "get" (download) command. - CommandGet = "get" ) // RegisteredGAPIs is a global map of all possible GAPIs which can be used. You @@ -55,15 +41,32 @@ func Register(name string, fn func() GAPI) { RegisteredGAPIs[name] = fn } -// CliInfo is the set of input values passed into the Cli method so that the -// GAPI can decide if it wants to activate, and if it does, the initial handles -// it needs to use to do so. -type CliInfo struct { - // CliContext is the struct that is used to transfer in user input. - CliContext *cli.Context +// Flags is some common data that comes from a higher-level command, and is used +// by a subcommand. By type circularity, the subcommands can't easily access the +// data in the parent command struct, so instead, the data that the parent wants +// to pass down, it wraps up in a struct (for API convenience) and sends it out. +type Flags struct { + Hostname *string + + Noop bool + Sema int +} + +// Info is the set of input values passed into the Cli method so that the GAPI +// can decide if it wants to activate, and if it does, the initial handles it +// needs to use to do so. +type Info struct { + // Args are the CLI args that are populated after parsing the args list. + // They need to be converted to the struct you are expecting to read it. + Args interface{} + + // Flags are the common data which is passed down into the sub command. + Flags *Flags + // Fs is the filesystem the Cli method should copy data into. It usually // copies *from* the local filesystem using standard io functionality. - Fs engine.Fs + Fs engine.Fs + Debug bool Logf func(format string, v ...interface{}) } @@ -99,18 +102,11 @@ type Next struct { // GAPI is a Graph API that represents incoming graphs and change streams. It is // the frontend interface that needs to be implemented to use the engine. type GAPI interface { - // CliFlags is passed a Command constant specifying which command it is - // requesting the flags for. If an invalid or unsupported command is - // passed in, simply return an empty list. Similarly, it is not required - // to ever return any flags, and the GAPI may always return an empty - // list. - CliFlags(string) []cli.Flag - // Cli is run on each GAPI to give it a chance to decide if it wants to // activate, and if it does, then it will return a deploy struct. During - // this time, it uses the CliInfo struct as useful information to decide + // this time, it uses the Info struct as useful information to decide // what to do. - Cli(*CliInfo) (*Deploy, error) + Cli(*Info) (*Deploy, error) // Init initializes the GAPI and passes in some useful data. Init(*Data) error @@ -130,28 +126,3 @@ type GAPI interface { // TODO: change Close to Cleanup Close() error } - -// GetInfo is the set of input values passed into the Get method for it to run. -type GetInfo struct { - // CliContext is the struct that is used to transfer in user input. - CliContext *cli.Context - - Noop bool - Sema int - Update bool - - Debug bool - Logf func(format string, v ...interface{}) -} - -// GettableGAPI represents additional methods that need to be implemented in -// this GAPI so that it can be used with the `get` Command. The methods in this -// interface are called independently from the rest of the GAPI interface, and -// you must not rely on shared state from those methods. Logically, this should -// probably be named "Getable", however the correct modern word is "Gettable". -type GettableGAPI interface { - GAPI // the base interface must be implemented - - // Get runs the get/download method. - Get(*GetInfo) error -} diff --git a/go.mod b/go.mod index c8413011..576c01d4 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( github.com/prometheus/client_golang v1.17.0 github.com/sanity-io/litter v1.5.5 github.com/spf13/afero v1.10.0 - github.com/urfave/cli/v2 v2.25.7 github.com/vishvananda/netlink v1.2.1-beta.2 go.etcd.io/etcd/api/v3 v3.5.10 go.etcd.io/etcd/client/pkg/v3 v3.5.10 @@ -44,6 +43,8 @@ require ( require ( cloud.google.com/go v0.110.4 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/alexflint/go-arg v1.4.3 // indirect + github.com/alexflint/go-scalar v1.1.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect diff --git a/go.sum b/go.sum index 987a002c..d017a9ea 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= +github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= +github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= +github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -782,12 +786,6 @@ github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIz github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/cli/v2 v2.20.2 h1:dKA0LUjznZpwmmbrc0pOgcLTEilnHeM8Av9Yng77gHM= -github.com/urfave/cli/v2 v2.20.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So= github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= diff --git a/integration/instance.go b/integration/instance.go index ff4f5b29..e870def4 100644 --- a/integration/instance.go +++ b/integration/instance.go @@ -428,7 +428,7 @@ func (obj *Instance) DeployLang(code string) error { cmdArgs := []string{ "deploy", // mode "--no-git", - "--seeds", obj.clientURL, + fmt.Sprintf("--seeds=%s", obj.clientURL), "lang", filename, } obj.Logf("run: %s %s", cmdName, strings.Join(cmdArgs, " ")) diff --git a/lang/core/world/schedule_func.go b/lang/core/world/schedule_func.go index bd573b91..ead0d9a8 100644 --- a/lang/core/world/schedule_func.go +++ b/lang/core/world/schedule_func.go @@ -17,8 +17,8 @@ // test with: // time ./mgmt run --hostname h1 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl -// time ./mgmt run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl -// time ./mgmt run --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl +// time ./mgmt run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl +// time ./mgmt run --hostname h3 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl // kill h2 (should see h1 and h3 pick [h1, h3] instead) // restart h2 (should see [h1, h3] as before) // kill h3 (should see h1 and h2 pick [h1, h2] instead) diff --git a/lang/gapi/gapi.go b/lang/gapi/gapi.go index 64a0befa..f03d4bf4 100644 --- a/lang/gapi/gapi.go +++ b/lang/gapi/gapi.go @@ -42,24 +42,40 @@ import ( "github.com/purpleidea/mgmt/util/errwrap" "github.com/spf13/afero" - "github.com/urfave/cli/v2" ) const ( // Name is the name of this frontend. Name = "lang" - - // flagModulePath is the name of the module-path flag. - flagModulePath = "module-path" - - // flagDownload is the name of the download flag. - flagDownload = "download" ) func init() { gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register } +// Args is the CLI parsing structure and type of the parsed result. +type Args struct { + // Input is the input mcl code or file path or any input specification. + Input string `arg:"positional,required"` + + // TODO: removed (temporarily?) + //Stdin bool `arg:"--stdin" help:"use passthrough stdin"` + + Download bool `arg:"--download" help:"download any missing imports"` + OnlyDownload bool `arg:"--only-download" help:"stop after downloading any missing imports"` + Update bool `arg:"--update" help:"update all dependencies to the latest versions"` + + OnlyUnify bool `arg:"--only-unify" help:"stop after type unification"` + SkipUnify bool `arg:"--skip-unify" help:"skip type unification"` + + Depth int `arg:"--depth" default:"-1" help:"max recursion depth limit (-1 is unlimited)"` + + // The default of 0 means any error is a failure by default. + Retry int `arg:"--depth" help:"max number of retries (-1 is unlimited)"` + + ModulePath string `arg:"--module-path,env:MGMT_MODULE_PATH" help:"choose the modules path (absolute)"` +} + // GAPI implements the main lang GAPI interface. type GAPI struct { InputURI string // input URI of code file system to run @@ -78,85 +94,9 @@ type GAPI struct { wg *sync.WaitGroup // sync group for tunnel go routines } -// CliFlags returns a list of flags used by the specified subcommand. -func (obj *GAPI) CliFlags(command string) []cli.Flag { - result := []cli.Flag{} - modulePath := &cli.StringFlag{ - Name: flagModulePath, - Value: "", // empty by default - Usage: "choose the modules path (absolute)", - EnvVars: []string{"MGMT_MODULE_PATH"}, - } - - // add this only to run (not needed for get or deploy) - if command == gapi.CommandRun { - runFlags := []cli.Flag{ - &cli.BoolFlag{ - Name: flagDownload, - Usage: "download any missing imports (as the get command does)", - }, - &cli.BoolFlag{ - Name: "update", - Usage: "update all dependencies to the latest versions", - }, - &cli.BoolFlag{ - Name: "only-unify", - Usage: "stop after type unification", - }, - } - result = append(result, runFlags...) - } - - if command == gapi.CommandRun || command == gapi.CommandDeploy { - flags := []cli.Flag{ - &cli.BoolFlag{ - Name: "skip-unify", - Usage: "skip type unification", - }, - } - result = append(result, flags...) - } - - switch command { - case gapi.CommandGet: - flags := []cli.Flag{ - &cli.IntFlag{ - Name: "depth d", - Value: -1, - Usage: "max recursion depth limit (-1 is unlimited)", - }, - &cli.IntFlag{ - Name: "retry r", - Value: 0, // any error is a failure by default - Usage: "max number of retries (-1 is unlimited)", - }, - //modulePath, // already defined below in fallthrough - } - result = append(result, flags...) - fallthrough // at the moment, we want the same code input arg... - case gapi.CommandRun: - fallthrough - case gapi.CommandDeploy: - flags := []cli.Flag{ - // TODO: removed (temporarily?) - //*cli.BoolFlag{ - // Name: "stdin", - // Usage: "use passthrough stdin", - //}, - modulePath, - } - result = append(result, flags...) - default: - return []cli.Flag{} - } - - return result -} - -// Cli takes a cli.Context, and returns our GAPI if activated. All arguments -// should take the prefix of the registered name. On activation, if there are -// any validation problems, you should return an error. If this was not -// activated, then you should return a nil GAPI and a nil error. This is passed +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. This is passed // in a functional file system interface. For standalone usage, this will be a // temporary memory-backed filesystem so that the same deploy API is used, and // for normal clustered usage, this will be the normal implementation which is @@ -167,29 +107,22 @@ func (obj *GAPI) CliFlags(command string) []cli.Flag { // or from `run` (both of which can activate this GAPI) is that `deploy` copies // to an etcdFs, and `run` copies to a memFs. All GAPI's run off of the fs that // is passed in. -func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { - c := cliInfo.CliContext - cliContext := c.Lineage()[1] - if cliContext == nil { - return nil, fmt.Errorf("could not get cli context") - } - fs := cliInfo.Fs // copy files from local filesystem *into* this fs... - prefix := "" // TODO: do we need this? - debug := cliInfo.Debug - logf := func(format string, v ...interface{}) { - cliInfo.Logf(Name+": "+format, v...) +func (obj *GAPI) Cli(info *gapi.Info) (*gapi.Deploy, error) { + args, ok := info.Args.(*Args) + if !ok { + // programming error + return nil, fmt.Errorf("could not convert to our struct") } - if l := c.NArg(); l != 1 { - if l > 1 { - return nil, fmt.Errorf("input program must be a single arg") - } - return nil, fmt.Errorf("must specify input program") + fs := info.Fs // copy files from local filesystem *into* this fs... + prefix := "" // TODO: do we need this? + debug := info.Debug + logf := func(format string, v ...interface{}) { + info.Logf(Name+": "+format, v...) } - input := c.Args().Get(0) // empty by default (don't set for deploy, only download) - modules := c.String(flagModulePath) + modules := args.ModulePath if modules != "" && (!strings.HasPrefix(modules, "/") || !strings.HasSuffix(modules, "/")) { return nil, fmt.Errorf("module path is not an absolute directory") } @@ -207,7 +140,7 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { // the fs input here is the local fs we're reading to get the files from // this is different from the fs variable which is our output dest!!! - output, err := inputs.ParseInput(input, localFs) + output, err := inputs.ParseInput(args.Input, localFs) if err != nil { return nil, errwrap.Wrapf(err, "could not activate an input parser") } @@ -224,15 +157,17 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { logf("behold, the AST: %+v", xast) } + // This runs the necessary downloads. It passes a downloader in, which + // can be used to pull down or update any missing imports. var downloader interfaces.Downloader - if c.IsSet(flagDownload) && c.Bool(flagDownload) { + if args.Download { downloadInfo := &interfaces.DownloadInfo{ Fs: downloadFs, // the local fs! // flags are passed in during Init() - Noop: cliContext.Bool("noop"), - Sema: cliContext.Int("sema"), - Update: c.Bool("update"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, + Update: args.Update, Debug: debug, Logf: func(format string, v ...interface{}) { @@ -242,8 +177,8 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { } // this fulfills the interfaces.Downloader interface downloader = &download.Downloader{ - Depth: c.Int("depth"), // default of infinite is -1 - Retry: c.Int("retry"), // infinite is -1 + Depth: args.Depth, // default of infinite is -1 + Retry: args.Retry, // infinite is -1 } if err := downloader.Init(downloadInfo); err != nil { return nil, errwrap.Wrapf(err, "could not initialize downloader") @@ -297,10 +232,14 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { return nil, errwrap.Wrapf(err, "could not interpolate AST") } + hostname := "" + if h := info.Flags.Hostname; h != nil { + hostname = *h // it's optional, since this value is not used... + } variables := map[string]interfaces.Expr{ "purpleidea": &ast.ExprStr{V: "hello world!"}, // james says hi // TODO: change to a func when we can change hostname dynamically! - "hostname": &ast.ExprStr{V: ""}, // NOTE: empty b/c not used + "hostname": &ast.ExprStr{V: hostname}, // NOTE: can be empty b/c not used } consts := ast.VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix! addback := vars.ConstNamespace + interfaces.ModuleSep // add it back... @@ -327,7 +266,12 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { return nil, errwrap.Wrapf(err, "could not set scope") } - if !c.Bool("skip-unify") { + // Previously the `get` command would stop here. + if args.OnlyDownload { + return nil, nil // success! + } + + if !args.SkipUnify { // apply type unification unificationLogf := func(format string, v ...interface{}) { if debug { // unification only has debug messages... @@ -349,13 +293,13 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { formatted = delta.Truncate(time.Millisecond).String() } if unifyErr != nil { - if c.Bool("only-unify") { + if args.OnlyUnify { logf("type unification failed after %s", formatted) } return nil, errwrap.Wrapf(unifyErr, "could not unify types") } - if c.Bool("only-unify") { + if args.OnlyUnify { logf("type unification succeeded in %s", formatted) return nil, nil // we end early } @@ -458,7 +402,7 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { // display the deploy fs tree if debug || true { // TODO: should this only be shown on debug? - logf("input: %s", input) + logf("input: %s", args.Input) tree, err := util.FsTree(fs, "/") if err != nil { return nil, err @@ -468,8 +412,8 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { return &gapi.Deploy{ Name: Name, - Noop: c.Bool("noop"), - Sema: c.Int("sema"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, GAPI: &GAPI{ InputURI: fs.URI(), // TODO: add properties here... @@ -691,142 +635,3 @@ func (obj *GAPI) Close() error { obj.initialized = false // closed = true return nil } - -// Get runs the necessary downloads. This basically runs the lexer, parser and -// sets the scope so that all the imports are followed. It passes a downloader -// in, which can be used to pull down or update any missing imports. This will -// also work when called with the download flag during a normal execution run. -func (obj *GAPI) Get(getInfo *gapi.GetInfo) error { - c := getInfo.CliContext - cliContext := c.Lineage()[1] - if cliContext == nil { - return fmt.Errorf("could not get cli context") - } - prefix := "" // TODO: do we need this? - debug := getInfo.Debug - logf := getInfo.Logf - - if l := c.NArg(); l != 1 { - if l > 1 { - return fmt.Errorf("input program must be a single arg") - } - return fmt.Errorf("must specify input program") - } - input := c.Args().Get(0) - - // empty by default (don't set for deploy, only download) - modules := c.String(flagModulePath) - if modules != "" && (!strings.HasPrefix(modules, "/") || !strings.HasSuffix(modules, "/")) { - return fmt.Errorf("module path is not an absolute directory") - } - - osFs := afero.NewOsFs() - readOnlyOsFs := afero.NewReadOnlyFs(osFs) // can't be readonly to dl! - //bp := afero.NewBasePathFs(osFs, base) // TODO: can this prevent parent dir access? - afs := &afero.Afero{Fs: readOnlyOsFs} // wrap so that we're implementing ioutil - localFs := &util.AferoFs{Afero: afs} // always the local fs - downloadAfs := &afero.Afero{Fs: osFs} - downloadFs := &util.AferoFs{Afero: downloadAfs} // TODO: use with a parent path preventer? - - // the fs input here is the local fs we're reading to get the files from - // this is different from the fs variable which is our output dest!!! - output, err := inputs.ParseInput(input, localFs) - if err != nil { - return errwrap.Wrapf(err, "could not activate an input parser") - } - - // no need to run recursion detection since this is the beginning - // TODO: do the paths need to be cleaned for "../" before comparison? - - logf("lexing/parsing...") - ast, err := parser.LexParse(bytes.NewReader(output.Main)) - if err != nil { - return errwrap.Wrapf(err, "could not generate AST") - } - if debug { - logf("behold, the AST: %+v", ast) - } - - downloadInfo := &interfaces.DownloadInfo{ - Fs: downloadFs, // the local fs! - - // flags are passed in during Init() - Noop: cliContext.Bool("noop"), - Sema: cliContext.Int("sema"), - Update: cliContext.Bool("update"), - - Debug: debug, - Logf: func(format string, v ...interface{}) { - // TODO: is this a sane prefix to use here? - logf("get: "+format, v...) - }, - } - // this fulfills the interfaces.Downloader interface - downloader := &download.Downloader{ - Depth: c.Int("depth"), // default of infinite is -1 - Retry: c.Int("retry"), // infinite is -1 - } - if err := downloader.Init(downloadInfo); err != nil { - return errwrap.Wrapf(err, "could not initialize downloader") - } - - importGraph, err := pgraph.NewGraph("importGraph") - if err != nil { - return err - } - importVertex := &pgraph.SelfVertex{ - Name: "", // first node is the empty string - Graph: importGraph, // store a reference to ourself - } - importGraph.AddVertex(importVertex) - - logf("init...") - // init and validate the structure of the AST - data := &interfaces.Data{ - // TODO: add missing fields here if/when needed - Fs: output.FS, // formerly: localFs // the local fs! - FsURI: output.FS.URI(), // formerly: localFs.URI() // TODO: is this right? - Base: output.Base, // base dir (absolute path) that this is rooted in - Files: output.Files, - Imports: importVertex, - Metadata: output.Metadata, - Modules: modules, - - LexParser: parser.LexParse, - Downloader: downloader, - StrInterpolater: interpolate.StrInterpolate, - //Local: obj.Local, // TODO: do we need this? - //World: obj.World, // TODO: do we need this? - - Prefix: prefix, - Debug: debug, - Logf: func(format string, v ...interface{}) { - // TODO: is this a sane prefix to use here? - logf("ast: "+format, v...) - }, - } - // some of this might happen *after* interpolate in SetScope or Unify... - if err := ast.Init(data); err != nil { - return errwrap.Wrapf(err, "could not init and validate AST") - } - - logf("interpolating...") - // interpolate strings and other expansionable nodes in AST - iast, err := ast.Interpolate() - if err != nil { - return errwrap.Wrapf(err, "could not interpolate AST") - } - - logf("building scope...") - // propagate the scope down through the AST... - // we use SetScope because it follows all of the imports through. i - // don't think we need to pass in an initial scope because the download - // operation shouldn't depend on any initial scope values, since those - // would all be runtime changes, and we do not support dynamic imports! - // XXX: Add non-empty scope? - if err := iast.SetScope(nil); err != nil { // empty initial scope! - return errwrap.Wrapf(err, "could not set scope") - } - - return nil // success! -} diff --git a/lang/interpret_test.go b/lang/interpret_test.go index 5e5e47b7..a1ff08e2 100644 --- a/lang/interpret_test.go +++ b/lang/interpret_test.go @@ -2107,7 +2107,7 @@ func TestAstFunc3(t *testing.T) { t.Logf("test #%d: graph: %+v", index, ograph) // setup converger - convergedTimeout := int64(5) + convergedTimeout := 5 converger := converger.New( convergedTimeout, ) diff --git a/lib/main.go b/lib/main.go index 819151b3..0afaa2a0 100644 --- a/lib/main.go +++ b/lib/main.go @@ -92,7 +92,7 @@ type Main struct { Sema int // add a semaphore with this lock count to each resource Graphviz string // output file for graphviz data GraphvizFilter string // graphviz filter to use - ConvergedTimeout int64 // approximately this many seconds of inactivity means we're in a converged state; -1 to disable + ConvergedTimeout int // approximately this many seconds of inactivity means we're in a converged state; -1 to disable ConvergedTimeoutNoExit bool // don't exit on converged timeout ConvergedStatusFile string // file to append converged status to MaxRuntime uint // exit after a maximum of approximately this many seconds diff --git a/main.go b/main.go index 3ae9c03f..98fdb25e 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ package main import ( + "context" _ "embed" "fmt" "os" @@ -29,8 +30,9 @@ import ( // These constants are some global variables that are used throughout the code. const ( - Debug = false // add additional log messages - Verbose = false // add extra log message output + tagline = "next generation config management" + debug = false // add additional log messages + verbose = false // add extra log message output ) // set at compile time @@ -60,13 +62,14 @@ func main() { Program: program, Version: version, Copying: copying, + Tagline: tagline, Flags: cliUtil.Flags{ - Debug: Debug, - Verbose: Verbose, + Debug: debug, + Verbose: verbose, }, Args: os.Args, } - if err := cli.CLI(data); err != nil { + if err := cli.CLI(context.Background(), data); err != nil { fmt.Println(err) os.Exit(1) return diff --git a/test/shell/etcd-clustersize.sh b/test/shell/etcd-clustersize.sh index fbd613f7..08668854 100755 --- a/test/shell/etcd-clustersize.sh +++ b/test/shell/etcd-clustersize.sh @@ -16,16 +16,16 @@ fi trap 'pkill -9 mgmt' EXIT $TIMEOUT "$MGMT" run --hostname h1 --tmp-prefix --no-pgp empty & -$TIMEOUT "$MGMT" run --hostname h2 --tmp-prefix --no-pgp --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 empty & -$TIMEOUT "$MGMT" run --hostname h3 --tmp-prefix --no-pgp --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 empty & +$TIMEOUT "$MGMT" run --hostname h2 --tmp-prefix --no-pgp --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 empty & +$TIMEOUT "$MGMT" run --hostname h3 --tmp-prefix --no-pgp --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 empty & # wait for everything to converge sleep 30s ETCDCTL_API=3 etcdctl --endpoints 127.0.0.1:2379 put /_mgmt/chooser/dynamicsize/idealclustersize 3 -$TIMEOUT "$MGMT" run --hostname h4 --tmp-prefix --no-pgp --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2385 --server-urls http://127.0.0.1:2386 empty & -$TIMEOUT "$MGMT" run --hostname h5 --tmp-prefix --no-pgp --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2387 --server-urls http://127.0.0.1:2388 empty & +$TIMEOUT "$MGMT" run --hostname h4 --tmp-prefix --no-pgp --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2385 --server-urls=http://127.0.0.1:2386 empty & +$TIMEOUT "$MGMT" run --hostname h5 --tmp-prefix --no-pgp --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2387 --server-urls=http://127.0.0.1:2388 empty & # wait for everything to converge sleep 30s diff --git a/test/shell/etcd-three-hosts-reversed.sh b/test/shell/etcd-three-hosts-reversed.sh index 6dce5776..11a8301b 100755 --- a/test/shell/etcd-three-hosts-reversed.sh +++ b/test/shell/etcd-three-hosts-reversed.sh @@ -13,11 +13,11 @@ $TIMEOUT "$MGMT" run --hostname h1 --tmp-prefix empty & pid1=$! sleep 45s # let it startup -$TIMEOUT "$MGMT" run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix empty & +$TIMEOUT "$MGMT" run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix empty & pid2=$! sleep 45s -$TIMEOUT "$MGMT" run --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix empty & +$TIMEOUT "$MGMT" run --hostname h3 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 --tmp-prefix empty & pid3=$! sleep 45s diff --git a/test/shell/etcd-three-hosts.sh b/test/shell/etcd-three-hosts.sh index 87d0294b..1c6c5e7c 100755 --- a/test/shell/etcd-three-hosts.sh +++ b/test/shell/etcd-three-hosts.sh @@ -13,11 +13,11 @@ $TIMEOUT "$MGMT" run --hostname h1 --tmp-prefix empty & pid1=$! sleep 15s # let it startup -$TIMEOUT "$MGMT" run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix empty & +$TIMEOUT "$MGMT" run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix empty & pid2=$! sleep 15s -$TIMEOUT "$MGMT" run --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix empty & +$TIMEOUT "$MGMT" run --hostname h3 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 --tmp-prefix empty & pid3=$! sleep 15s diff --git a/test/shell/etcd-two-hosts-reversed.sh b/test/shell/etcd-two-hosts-reversed.sh index f4f80cdd..f28dc291 100755 --- a/test/shell/etcd-two-hosts-reversed.sh +++ b/test/shell/etcd-two-hosts-reversed.sh @@ -13,7 +13,7 @@ $TIMEOUT "$MGMT" run --hostname h1 --tmp-prefix empty & pid1=$! sleep 45s # let it startup -$TIMEOUT "$MGMT" run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix empty & +$TIMEOUT "$MGMT" run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix empty & pid2=$! sleep 45s diff --git a/test/shell/etcd-two-hosts.sh b/test/shell/etcd-two-hosts.sh index e527ff50..9f38c1e0 100755 --- a/test/shell/etcd-two-hosts.sh +++ b/test/shell/etcd-two-hosts.sh @@ -13,7 +13,7 @@ $TIMEOUT "$MGMT" run --hostname h1 --tmp-prefix empty & pid1=$! sleep 15s # let it startup -$TIMEOUT "$MGMT" run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix empty & +$TIMEOUT "$MGMT" run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix empty & pid2=$! sleep 15s diff --git a/test/shell/exchange.sh b/test/shell/exchange.sh index 953155b9..7ed1376a 100755 --- a/test/shell/exchange.sh +++ b/test/shell/exchange.sh @@ -14,16 +14,16 @@ set -o pipefail $TIMEOUT "$MGMT" run --hostname h1 --tmp-prefix --no-pgp empty & pid1=$! sleep 10s -$TIMEOUT "$MGMT" run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp empty & +$TIMEOUT "$MGMT" run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix --no-pgp empty & pid2=$! sleep 10s -$TIMEOUT "$MGMT" run --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp empty & +$TIMEOUT "$MGMT" run --hostname h3 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 --tmp-prefix --no-pgp empty & pid3=$! sleep 10s -$TIMEOUT "$MGMT" run --hostname h4 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2385 --server-urls http://127.0.0.1:2386 --tmp-prefix --no-pgp empty & +$TIMEOUT "$MGMT" run --hostname h4 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2385 --server-urls=http://127.0.0.1:2386 --tmp-prefix --no-pgp empty & pid4=$! sleep 10s -$TIMEOUT "$MGMT" deploy --no-git --seeds http://127.0.0.1:2379 lang exchange0.mcl +$TIMEOUT "$MGMT" deploy --no-git --seeds=http://127.0.0.1:2379 lang exchange0.mcl # kill servers on error/exit #trap 'pkill -9 mgmt' EXIT diff --git a/test/shell/exchange0.mcl b/test/shell/exchange0.mcl index ded8a8b4..eb31883d 100644 --- a/test/shell/exchange0.mcl +++ b/test/shell/exchange0.mcl @@ -1,10 +1,10 @@ # run this example with these commands # watch -n 0.1 'tail *' # run this in /tmp/mgmt/ # time ./mgmt run --hostname h1 --tmp-prefix --no-pgp empty -# time ./mgmt run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp empty -# time ./mgmt run --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp empty -# time ./mgmt run --hostname h4 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2385 --server-urls http://127.0.0.1:2386 --tmp-prefix --no-pgp empty -# time ./mgmt deploy --no-git --seeds http://127.0.0.1:2379 lang examples/lang/exchange0.mcl +# time ./mgmt run --hostname h2 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2381 --server-urls=http://127.0.0.1:2382 --tmp-prefix --no-pgp empty +# time ./mgmt run --hostname h3 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2383 --server-urls=http://127.0.0.1:2384 --tmp-prefix --no-pgp empty +# time ./mgmt run --hostname h4 --seeds=http://127.0.0.1:2379 --client-urls=http://127.0.0.1:2385 --server-urls=http://127.0.0.1:2386 --tmp-prefix --no-pgp empty +# time ./mgmt deploy --no-git --seeds=http://127.0.0.1:2379 lang examples/lang/exchange0.mcl import "sys" import "world" diff --git a/yamlgraph/gapi.go b/yamlgraph/gapi.go index 1a180833..620c0508 100644 --- a/yamlgraph/gapi.go +++ b/yamlgraph/gapi.go @@ -26,13 +26,12 @@ import ( "github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/util/errwrap" - - "github.com/urfave/cli/v2" ) const ( // Name is the name of this frontend. Name = "yaml" + // Start is the entry point filename that we use. It is arbitrary. Start = "/start.yaml" ) @@ -41,6 +40,12 @@ func init() { gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register } +// Args is the CLI parsing structure and type of the parsed result. +type Args struct { + // Input is the input yaml code or file path or any input specification. + Input string `arg:"positional,required"` +} + // GAPI implements the main yamlgraph GAPI interface. type GAPI struct { InputURI string // input URI of file system containing yaml graph to use @@ -51,56 +56,36 @@ type GAPI struct { wg sync.WaitGroup // sync group for tunnel go routines } -// CliFlags returns a list of flags used by the specified subcommand. -func (obj *GAPI) CliFlags(command string) []cli.Flag { - switch command { - case gapi.CommandRun: - fallthrough - case gapi.CommandDeploy: - return []cli.Flag{} - //case gapi.CommandGet: - default: - return []cli.Flag{} +// Cli takes an *Info struct, and returns our deploy if activated, and if there +// are any validation problems, you should return an error. If there is no +// deploy, then you should return a nil deploy and a nil error. +func (obj *GAPI) Cli(info *gapi.Info) (*gapi.Deploy, error) { + args, ok := info.Args.(*Args) + if !ok { + // programming error + return nil, fmt.Errorf("could not convert to our struct") } -} -// Cli takes a cli.Context, and returns our GAPI if activated. All arguments -// should take the prefix of the registered name. On activation, if there are -// any validation problems, you should return an error. If this was not -// activated, then you should return a nil GAPI and a nil error. -func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { - c := cliInfo.CliContext - fs := cliInfo.Fs - //debug := cliInfo.Debug + fs := info.Fs + //debug := info.Debug //logf := func(format string, v ...interface{}) { - // cliInfo.Logf(Name + ": "+format, v...) + // info.Logf(Name + ": "+format, v...) //} - if l := c.NArg(); l != 1 { - if l > 1 { - return nil, fmt.Errorf("input program must be a single arg") - } - return nil, fmt.Errorf("must specify input program") - } - s := c.Args().Get(0) - if s == "" { - return nil, fmt.Errorf("input yaml is empty") - } - writeableFS, ok := fs.(engine.WriteableFS) if !ok { return nil, fmt.Errorf("the FS was not writeable") } // single file input only - if err := gapi.CopyFileToFs(writeableFS, s, Start); err != nil { - return nil, errwrap.Wrapf(err, "can't copy yaml from `%s` to `%s`", s, Start) + if err := gapi.CopyFileToFs(writeableFS, args.Input, Start); err != nil { + return nil, errwrap.Wrapf(err, "can't copy yaml from `%s` to `%s`", args.Input, Start) } return &gapi.Deploy{ Name: Name, - Noop: c.Bool("noop"), - Sema: c.Int("sema"), + Noop: info.Flags.Noop, + Sema: info.Flags.Sema, GAPI: &GAPI{ InputURI: fs.URI(), // TODO: add properties here...