lib: Split off the CLI portions into a separate package

This cleans things up a bit more and forces the lib package to not
contain any accidental CLI parsing code.
This commit is contained in:
James Shubin
2024-02-22 19:50:44 -05:00
parent 70b5ed7067
commit abe3e0e7a5
8 changed files with 47 additions and 30 deletions

View File

@@ -1,406 +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 lib
import (
"fmt"
"log"
"os"
"sort"
"github.com/purpleidea/mgmt/gapi"
_ "github.com/purpleidea/mgmt/lang" // import so the GAPI registers
_ "github.com/purpleidea/mgmt/langpuppet"
_ "github.com/purpleidea/mgmt/puppet"
_ "github.com/purpleidea/mgmt/yamlgraph"
"github.com/urfave/cli/v2"
)
// CLIArgs is a struct of values that we pass to the main CLI function.
type CLIArgs struct {
Program string
Version string
Copying string
Flags Flags
}
// CLI is the entry point for using mgmt normally from the CLI.
func CLI(cliArgs *CLIArgs) error {
// test for sanity
if cliArgs == nil {
return fmt.Errorf("this CLI was not run correctly")
}
if cliArgs.Program == "" || cliArgs.Version == "" {
return fmt.Errorf("program was not compiled correctly, see Makefile")
}
if cliArgs.Copying == "" {
return fmt.Errorf("program copyrights we're 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
// 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",
},
}
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",
},
}
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",
},
}
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 = cliArgs.Program // App.name and App.version pass these values through
app.Version = cliArgs.Version
app.Usage = "next generation config management"
app.Metadata = map[string]interface{}{ // additional flags
"flags": cliArgs.Flags,
}
// if no app.Command is specified
app.Action = func(c *cli.Context) error {
// print the license
if c.Bool("license") {
fmt.Printf("%s", cliArgs.Copying)
return nil
}
// print help if no flags are set
cli.ShowAppHelp(c)
return nil
}
// global flags
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "license",
Usage: "prints the software license",
},
}
app.Commands = []*cli.Command{
//{
// Name: gapi.CommandTODO,
// Aliases: []string{"TODO"},
// Usage: "TODO",
// Action: TODO,
// Flags: TODOFlags,
//},
}
// 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(os.Args)
}

View File

@@ -1,200 +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 lib
import (
"context"
"fmt"
"log"
"os"
"github.com/purpleidea/mgmt/etcd/client"
"github.com/purpleidea/mgmt/etcd/deployer"
etcdfs "github.com/purpleidea/mgmt/etcd/fs"
"github.com/purpleidea/mgmt/gapi"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/pborman/uuid"
"github.com/urfave/cli/v2"
git "gopkg.in/src-d/go-git.v4"
)
const (
// MetadataPrefix is the etcd prefix where all our fs superblocks live.
MetadataPrefix = "/fs"
// StoragePrefix is the etcd prefix where all our fs data lives.
StoragePrefix = "/storage"
)
// 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")
}
program, version := safeProgram(c.App.Name), c.App.Version
var flags Flags
var debug bool
if val, exists := c.App.Metadata["flags"]; exists {
if f, ok := val.(Flags); ok {
flags = f
debug = flags.Debug
}
}
Logf := func(format string, v ...interface{}) {
log.Printf("deploy: "+format, v...)
}
hello(program, version, flags) // say hello!
defer Logf("goodbye!")
var hash, pHash string
if !cliContext.Bool("no-git") {
wd, err := os.Getwd()
if err != nil {
return 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")
}
head, err := repo.Head()
if err != nil {
return errwrap.Wrapf(err, "could not read git HEAD")
}
hash = head.Hash().String() // current commit id
Logf("hash: %s", hash)
lo := &git.LogOptions{
From: head.Hash(),
}
commits, err := repo.Log(lo)
if err != nil {
return 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!
}
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") {
pHash = "" // don't check this :(
}
if hash == "" {
return 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
NS,
)
if err := etcdClient.Init(); err != nil {
return errwrap.Wrapf(err, "client Init failed")
}
defer func() {
err := errwrap.Wrapf(etcdClient.Close(), "client Close failed")
if err != nil {
// TODO: cause the final exit code to be non-zero
Logf("client cleanup error: %+v", err)
}
}()
simpleDeploy := &deployer.SimpleDeploy{
Client: etcdClient,
Debug: debug,
Logf: func(format string, v ...interface{}) {
Logf("deploy: "+format, v...)
},
}
if err := simpleDeploy.Init(); err != nil {
return errwrap.Wrapf(err, "deploy Init failed")
}
defer func() {
err := errwrap.Wrapf(simpleDeploy.Close(), "deploy Close failed")
if err != nil {
// TODO: cause the final exit code to be non-zero
Logf("deploy cleanup error: %+v", err)
}
}()
// get max id (from all the previous deploys)
max, err := simpleDeploy.GetMaxDeployID(context.TODO())
if err != nil {
return errwrap.Wrapf(err, "error getting max deploy id")
}
// find the latest id
var id = max + 1 // next id
Logf("previous max deploy id: %d", max)
etcdFs := &etcdfs.Fs{
Client: etcdClient,
// TODO: using a uuid is meant as a temporary measure, i hate them
Metadata: MetadataPrefix + fmt.Sprintf("/deploy/%d-%s", id, uniqueid),
DataPrefix: StoragePrefix,
Debug: debug,
Logf: func(format string, v ...interface{}) {
Logf("fs: "+format, v...)
},
}
cliInfo := &gapi.CliInfo{
CliContext: c, // don't pass in the parent context
Fs: etcdFs,
Debug: 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)
if err != nil {
return errwrap.Wrapf(err, "cli parse error")
}
if deploy == nil { // not used
return fmt.Errorf("not enough information specified")
}
// redundant
deploy.Noop = cliContext.Bool("noop")
deploy.Sema = cliContext.Int("sema")
str, err := deploy.ToB64()
if err != nil {
return 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)
}
Logf("success, id: %d", id)
return nil
}

View File

@@ -1,73 +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 lib
import (
"fmt"
"log"
"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 := safeProgram(c.App.Name), c.App.Version
var flags Flags
var debug bool
if val, exists := c.App.Metadata["flags"]; exists {
if f, ok := val.(Flags); ok {
flags = f
debug = flags.Debug
}
}
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

@@ -1,46 +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 lib
import (
"fmt"
"log"
"os"
"time"
)
func hello(program, version string, flags Flags) {
var start = time.Now().UnixNano()
logFlags := log.LstdFlags
if flags.Debug {
logFlags = logFlags + log.Lshortfile
}
logFlags = logFlags - log.Ldate // remove the date for now
log.SetFlags(logFlags)
log.SetOutput(os.Stderr)
if program == "" {
program = "<unknown>"
}
fmt.Println(fmt.Sprintf("This is: %s, version: %s", program, version))
fmt.Println("Copyright (C) 2013-2024+ James Shubin and the project contributors")
fmt.Println("Written by James Shubin <james@shubin.ca> and the project contributors")
log.Printf("main: start: %v", start)
}

View File

@@ -55,6 +55,12 @@ import (
const (
// NS is the root namespace for etcd operations. All keys must use it!
NS = "/_mgmt" // must not end with a slash!
// MetadataPrefix is the etcd prefix where all our fs superblocks live.
MetadataPrefix = "/fs"
// StoragePrefix is the etcd prefix where all our fs data lives.
StoragePrefix = "/storage"
)
// Flags are some constant flags which are used throughout the program.
@@ -202,9 +208,6 @@ func (obj *Main) Run() error {
log.Printf("main: "+format, v...)
}
hello(obj.Program, obj.Version, obj.Flags) // say hello!
defer Logf("goodbye!")
exitCtx := obj.exit.Context() // local exit signal
defer obj.exit.Done(nil) // ensure this gets called even if Exit doesn't

View File

@@ -1,196 +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 lib
import (
"fmt"
"log"
"os"
"os/signal"
"sync"
"syscall"
"github.com/purpleidea/mgmt/gapi"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
"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")
}
obj := &Main{}
obj.Program, obj.Version = safeProgram(c.App.Name), c.App.Version
if val, exists := c.App.Metadata["flags"]; exists {
if flags, ok := val.(Flags); ok {
obj.Flags = flags
}
}
if h := cliContext.String("hostname"); cliContext.IsSet("hostname") && h != "" {
obj.Hostname = &h
}
if s := cliContext.String("prefix"); cliContext.IsSet("prefix") && s != "" {
obj.Prefix = &s
}
obj.TmpPrefix = cliContext.Bool("tmp-prefix")
obj.AllowTmpPrefix = cliContext.Bool("allow-tmp-prefix")
// create a memory backed temporary filesystem for storing runtime data
mmFs := afero.NewMemMapFs()
afs := &afero.Afero{Fs: mmFs} // wrap so that we're implementing ioutil
standaloneFs := &util.AferoFs{Afero: afs}
obj.DeployFs = standaloneFs
cliInfo := &gapi.CliInfo{
CliContext: c, // don't pass in the parent context
Fs: standaloneFs,
Debug: obj.Flags.Debug,
Logf: func(format string, v ...interface{}) {
log.Printf("cli: "+format, v...)
},
}
deploy, err := gapiObj.Cli(cliInfo)
if err != nil {
return errwrap.Wrapf(err, "cli parse error")
}
if c.Bool("only-unify") && deploy == nil {
return nil // we end early
}
obj.Deploy = deploy
if obj.Deploy == nil {
// nobody activated, but we'll still watch the etcd deploy chan,
// and if there is deployed code that's ready to run, we'll run!
log.Printf("main: no frontend selected (no GAPI activated)")
}
obj.NoWatch = cliContext.Bool("no-watch")
obj.NoStreamWatch = cliContext.Bool("no-stream-watch")
obj.NoDeployWatch = cliContext.Bool("no-deploy-watch")
obj.Noop = cliContext.Bool("noop")
obj.Sema = cliContext.Int("sema")
obj.Graphviz = cliContext.String("graphviz")
obj.GraphvizFilter = cliContext.String("graphviz-filter")
obj.ConvergedTimeout = cliContext.Int64("converged-timeout")
obj.ConvergedTimeoutNoExit = cliContext.Bool("converged-timeout-no-exit")
obj.ConvergedStatusFile = cliContext.String("converged-status-file")
obj.MaxRuntime = uint(cliContext.Int("max-runtime"))
obj.Seeds = cliContext.StringSlice("seeds")
obj.ClientURLs = cliContext.StringSlice("client-urls")
obj.ServerURLs = cliContext.StringSlice("server-urls")
obj.AdvertiseClientURLs = cliContext.StringSlice("advertise-client-urls")
obj.AdvertiseServerURLs = cliContext.StringSlice("advertise-server-urls")
obj.IdealClusterSize = cliContext.Int("ideal-cluster-size")
obj.NoServer = cliContext.Bool("no-server")
obj.NoNetwork = cliContext.Bool("no-network")
obj.NoPgp = cliContext.Bool("no-pgp")
if kp := cliContext.String("pgp-key-path"); cliContext.IsSet("pgp-key-path") {
obj.PgpKeyPath = &kp
}
if us := cliContext.String("pgp-identity"); cliContext.IsSet("pgp-identity") {
obj.PgpIdentity = &us
}
obj.Prometheus = cliContext.Bool("prometheus")
obj.PrometheusListen = cliContext.String("prometheus-listen")
if err := obj.Validate(); err != nil {
return err
}
if err := obj.Init(); err != nil {
return err
}
// install the exit signal handler
wg := &sync.WaitGroup{}
defer wg.Wait()
exit := make(chan struct{})
defer close(exit)
wg.Add(1)
go func() {
defer wg.Done()
// must have buffer for max number of signals
signals := make(chan os.Signal, 3+1) // 3 * ^C + 1 * SIGTERM
signal.Notify(signals, os.Interrupt) // catch ^C
//signal.Notify(signals, os.Kill) // catch signals
signal.Notify(signals, syscall.SIGTERM)
var count uint8
for {
select {
case sig := <-signals: // any signal will do
if sig != os.Interrupt {
log.Printf("interrupted by signal")
obj.Interrupt(fmt.Errorf("killed by %v", sig))
return
}
switch count {
case 0:
log.Printf("interrupted by ^C")
obj.Exit(nil)
case 1:
log.Printf("interrupted by ^C (fast pause)")
obj.FastExit(nil)
case 2:
log.Printf("interrupted by ^C (hard interrupt)")
obj.Interrupt(nil)
}
count++
case <-exit:
return
}
}
}()
reterr := obj.Run()
if reterr != nil {
// log the error message returned
if obj.Flags.Debug {
log.Printf("main: %+v", reterr)
}
}
if err := obj.Close(); err != nil {
if obj.Flags.Debug {
log.Printf("main: Close: %+v", err)
}
if reterr == nil {
return err
}
reterr = errwrap.Append(reterr, err)
}
return reterr
}

View File

@@ -1,35 +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 lib
import (
"strings"
)
// safeProgram returns the correct program string when given a buggy variant.
func safeProgram(program string) string {
// FIXME: in sub commands, the cli package appends a space and the sub
// command name at the end. hack around this by only using the first bit
// see: https://github.com/urfave/cli/issues/783 for more details...
split := strings.Split(program, " ")
program = split[0]
//if program == "" {
// program = "<unknown>"
//}
return program
}