From 589a5f9aeb5663b3e00107f3ae8a311903c20298 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Fri, 1 Mar 2024 18:09:06 -0500 Subject: [PATCH] cli, lib, lang: Port to new cli library The new version of the urfave/cli library is moving to generics, and it's completely unclear to me why this is an improvement. Their new API is very complicated to understand, which for me, defeats the purpose of golang. In parallel, I needed to do some upcoming cli API refactoring, so this was a good time to look into new libraries. After a review of the landscape, I found the alexflint/go-arg library which has a delightfully elegant API. It does have a few rough edges, but it's otherwise very usable, and I think it would be straightforward to add features and fix issues. Thanks Alex! --- cli/cli.go | 442 +++++------------------- cli/deploy.go | 134 ++++--- cli/get.go | 74 ---- cli/run.go | 230 ++++++++---- cli/util/util.go | 21 ++ converger/converger.go | 8 +- docs/faq.md | 26 ++ etcd/etcd.go | 8 +- examples/lang/exchange0.mcl | 8 +- examples/lib/exec-send-recv.go | 31 +- examples/lib/libmgmt-subgraph0.go | 31 +- examples/lib/libmgmt-subgraph1.go | 31 +- examples/lib/libmgmt2.go | 31 +- examples/lib/libmgmt3.go | 31 +- gapi/empty/empty.go | 19 +- gapi/gapi.go | 81 ++--- go.mod | 3 +- go.sum | 10 +- integration/instance.go | 2 +- lang/core/world/schedule_func.go | 4 +- lang/gapi/gapi.go | 319 ++++------------- lang/interpret_test.go | 2 +- lib/main.go | 2 +- main.go | 13 +- test/shell/etcd-clustersize.sh | 8 +- test/shell/etcd-three-hosts-reversed.sh | 4 +- test/shell/etcd-three-hosts.sh | 4 +- test/shell/etcd-two-hosts-reversed.sh | 2 +- test/shell/etcd-two-hosts.sh | 2 +- test/shell/exchange.sh | 8 +- test/shell/exchange0.mcl | 8 +- yamlgraph/gapi.go | 59 ++-- 32 files changed, 609 insertions(+), 1047 deletions(-) delete mode 100644 cli/get.go 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...