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!
This commit is contained in:
James Shubin
2024-03-01 18:09:06 -05:00
parent e767655ede
commit 589a5f9aeb
32 changed files with 609 additions and 1047 deletions

View File

@@ -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{}

View File

@@ -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
}

View File

@@ -1,74 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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
}

3
go.mod
View File

@@ -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

10
go.sum
View File

@@ -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=

View File

@@ -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, " "))

View File

@@ -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)

View File

@@ -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!
}

View File

@@ -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,
)

View File

@@ -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

13
main.go
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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...