cli, entry, lang: Add an entry package for embeddable CLI's
This adds a new entry package that allows embedded programs to exist inside of mgmt. This took a lot of refactoring to get the API right, but I think it's incredibly elegant now. There is a chance we tweak things a bit, but it's a good first start. All-in-one programs are coming soon!
This commit is contained in:
@@ -31,6 +31,12 @@ import (
|
||||
"github.com/alexflint/go-arg"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if _, err := arg.NewParser(arg.Config{}, &Args{}); err != nil { // sanity check
|
||||
panic(errwrap.Wrapf(err, "invalid args cli struct"))
|
||||
}
|
||||
}
|
||||
|
||||
// CLI is the entry point for using mgmt normally from the CLI.
|
||||
func CLI(ctx context.Context, data *cliUtil.Data) error {
|
||||
// test for sanity
|
||||
|
||||
297
entry/entry.go
Normal file
297
entry/entry.go
Normal file
@@ -0,0 +1,297 @@
|
||||
// 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 entry provides an API to kicking off the initial binary execution.
|
||||
// The functions and data structures in this library can be used to package your
|
||||
// own custom entry point as a custom application.
|
||||
// TODO: Should this be nested inside of lang/ or can it be used for all GAPI's?
|
||||
package entry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/purpleidea/mgmt/cli"
|
||||
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"github.com/alexflint/go-arg"
|
||||
)
|
||||
|
||||
// registeredData is the single "registered" entry point that we built with. You
|
||||
// cannot have more than one currently.
|
||||
// TODO: In the future we could have more than one registered and each could
|
||||
// appear under a top-level "embedded" subcommand if we decided not to have a
|
||||
// "default" singleton registered.
|
||||
var registeredData *Data
|
||||
|
||||
// Register takes input data and stores it for lookup by the top-level main
|
||||
// function. Register is commonly called in the init() method of the module that
|
||||
// defined it, which happens at program startup. Build flags should be used to
|
||||
// determine which Register gets to run. Only one entry can be registered at a
|
||||
// time. There is no matching Unregister function at this time.
|
||||
func Register(data *Data) {
|
||||
if registeredData != nil {
|
||||
panic("an entry is already registered")
|
||||
}
|
||||
if err := data.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
registeredData = data
|
||||
}
|
||||
|
||||
// Data is what a prospective standalone entry program must specify to our API.
|
||||
type Data struct {
|
||||
// Program is the name of this program, usually set at compile time.
|
||||
Program string
|
||||
|
||||
// Version is the version of this program, usually set at compile time.
|
||||
Version string
|
||||
|
||||
// TODO: flags struct API here?
|
||||
Debug bool
|
||||
//Verbose bool
|
||||
|
||||
// Args is the CLI struct to use. This takes the format of the go-arg
|
||||
// API. Keep in mind that these values will be added on to the normal
|
||||
// run subcommand with frontend that is chosen. Make sure you don't add
|
||||
// anything that would conflict with that. Of note, a new subcommand is
|
||||
// probably not what you want. To do more complicated things, you will
|
||||
// need to implement a different custom API with Customizable.
|
||||
Args interface{}
|
||||
|
||||
// Frontend is the name of the GAPI to run.
|
||||
Frontend string
|
||||
|
||||
// Top is the initial input or code to run.
|
||||
Top []byte
|
||||
|
||||
// Customizable is an additional API you can implement to have tighter
|
||||
// control over how the entry executes mgmt.
|
||||
Custom Customizable
|
||||
}
|
||||
|
||||
// Validate verifies that the structure has acceptable data stored within.
|
||||
func (obj *Data) Validate() error {
|
||||
if obj == nil {
|
||||
return fmt.Errorf("data is nil")
|
||||
}
|
||||
if obj.Program == "" {
|
||||
return fmt.Errorf("program is empty")
|
||||
}
|
||||
if obj.Version == "" {
|
||||
return fmt.Errorf("version is empty")
|
||||
}
|
||||
if _, err := arg.NewParser(arg.Config{}, obj.Args); err != nil { // sanity check
|
||||
return errwrap.Wrapf(err, "invalid args cli struct")
|
||||
}
|
||||
if obj.Frontend == "" {
|
||||
return fmt.Errorf("frontend is empty")
|
||||
}
|
||||
if len(obj.Top) == 0 {
|
||||
return fmt.Errorf("top is empty")
|
||||
}
|
||||
//if obj.Custom == nil { // this is allowed!
|
||||
// return fmt.Errorf("custom is nil")
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup returns the runner that implements the complex plumbing to kick off
|
||||
// the run. If one has not been registered, then this will error.
|
||||
func Lookup() (*Runner, error) {
|
||||
if registeredData == nil {
|
||||
return nil, fmt.Errorf("could not find a registered entry")
|
||||
}
|
||||
|
||||
return &Runner{
|
||||
data: registeredData, // *Data
|
||||
}, nil
|
||||
}
|
||||
|
||||
// runnerArgs are some default args that get forced into the arg parser.
|
||||
type runnerArgs struct {
|
||||
License bool `arg:"--license" help:"display the license and exit"`
|
||||
|
||||
// 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 *runnerArgs) Version() string {
|
||||
return obj.version
|
||||
}
|
||||
|
||||
// Description returns a description string. Implementing this signature is part
|
||||
// of the API for the cli library.
|
||||
func (obj *runnerArgs) Description() string {
|
||||
return obj.description
|
||||
}
|
||||
|
||||
// Runner implements the complex plumbing that kicks off the run. The top-level
|
||||
// main function should call our Run method. This is all private because it
|
||||
// should only get returned by the Lookup method and used as-is.
|
||||
type Runner struct {
|
||||
data *Data
|
||||
}
|
||||
|
||||
// CLI is the entry point for using any embedded package from the CLI. It is
|
||||
// used as the main entry point from the top-level main function and kicks-off
|
||||
// the CLI parser.
|
||||
//
|
||||
// XXX: This function is analogous to the cli/cli.go:CLI() function. Could it be
|
||||
// shared with what's there already or extended to get used for the method too?
|
||||
func (obj *Runner) CLI(ctx context.Context, data *cliUtil.Data) error {
|
||||
// obj.data comes from what the user Registered(): trust this less
|
||||
// cli.data comes from what the mgmt compiler specified: trust this more
|
||||
|
||||
// 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")
|
||||
}
|
||||
if data.Copying == "" {
|
||||
return fmt.Errorf("program copyrights were removed, can't run")
|
||||
}
|
||||
|
||||
// TODO: If obj.data has any special API's for getting program name,
|
||||
// version, or anything else in particular, we can use those values to
|
||||
// override what we get at compile time from main.main() that comes in
|
||||
// here in our *cliUtil.Data input.
|
||||
|
||||
config := arg.Config{
|
||||
Program: data.Program,
|
||||
}
|
||||
|
||||
runnerArgs := &runnerArgs{}
|
||||
runnerArgs.version = data.Version // copy this in
|
||||
//runnerArgs.description = data.Tagline
|
||||
|
||||
runArgs := &cli.RunArgs{} // This entry API is based on the `run` cli!
|
||||
|
||||
// You can pass in more than one struct and they are all used. Neat!
|
||||
// XXX: This generates sub-optimal help text because we mask the subcmd
|
||||
// XXX: Improve the arg parser library so that we can produce good help
|
||||
parser, err := arg.NewParser(config, runnerArgs, runArgs, obj.data.Args) // this is the struct
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
osArgs := data.Args[1:] // XXX: args[0] needs to be dropped
|
||||
if obj.data.Custom != nil {
|
||||
osArgs = append(osArgs, obj.data.Frontend) // HACK: add on the frontend sub-command name
|
||||
osArgs = append(osArgs, "''") // HACK: add on fake input for sub-command
|
||||
}
|
||||
err = parser.Parse(osArgs)
|
||||
if err == arg.ErrHelp {
|
||||
parser.WriteHelp(os.Stdout)
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// display the license
|
||||
if runnerArgs.License {
|
||||
fmt.Printf("%s", data.Copying) // file comes with a trailing nl
|
||||
return nil
|
||||
}
|
||||
|
||||
var args interface{}
|
||||
if cmd := runArgs.RunEmpty; cmd != nil {
|
||||
//name = cliUtil.LookupSubcommand(runArgs, cmd) // "empty"
|
||||
args = cmd
|
||||
// nothing to overwrite here
|
||||
}
|
||||
if cmd := runArgs.RunLang; cmd != nil {
|
||||
//name = cliUtil.LookupSubcommand(runArgs, cmd) // "lang"
|
||||
args = cmd
|
||||
//fmt.Printf("Input: %+v\n", cmd.Input) // :(
|
||||
cmd.Input = string(obj.data.Top) // overwrite
|
||||
}
|
||||
if cmd := runArgs.RunYaml; cmd != nil {
|
||||
//name = cliUtil.LookupSubcommand(runArgs, cmd) // "yaml"
|
||||
args = cmd
|
||||
cmd.Input = string(obj.data.Top) // overwrite
|
||||
}
|
||||
_ = args
|
||||
|
||||
if obj.data.Custom != nil {
|
||||
// The obj.data.Args value is our own Args struct which we can
|
||||
// already have access to by holding on to it ourselves when we
|
||||
// create it! So no need to pass it back in to ourselves...
|
||||
//libConfig, err := obj.data.Custom.Customize(obj.data.Args)
|
||||
// Instead pass in something more useful that we don't have!
|
||||
// This will contain the parsed result of *lib.Config that the
|
||||
// cmdline arg parser already parsed! This function can now
|
||||
// modify it based on it's own `args` (obj.data.Args) and pass
|
||||
// out an "improved" *lib.Config structure to actually use!
|
||||
//libConfig, err := obj.data.Custom.Customize(&runArgs.Config) // TODO: I wish runArgs was a ptr
|
||||
// But unbelievably (and more awkwardly) we can do even better
|
||||
// by passing in the full runArgs struct (which includes the
|
||||
// frontend parsing) and then we can modify any part of that
|
||||
// whole thing and return it back. That's what we then can use!
|
||||
runArgs, err = obj.data.Custom.Customize(runArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if runArgs == nil {
|
||||
return fmt.Errorf("entry broke the runArgs struct")
|
||||
}
|
||||
//if libConfig != nil {
|
||||
// runArgs.Config = *libConfig // TODO: I wish runArgs took a ptr
|
||||
//}
|
||||
}
|
||||
|
||||
if ok, err := runArgs.Run(ctx, data); err != nil {
|
||||
return err
|
||||
} else if !ok { // did we activate one of the commands?
|
||||
return fmt.Errorf("command could not execute")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Customizable is an additional API you can implement to have tighter control
|
||||
// over how the entry executes mgmt.
|
||||
// TODO: add an API with: func(arg.Config) (*arg.Parser, error) ?
|
||||
type Customizable interface {
|
||||
// Customize takes in the full parsed struct, and returns the RunArgs
|
||||
// that we should use for the run operation.
|
||||
// TODO: should the input type be *cli.RunArgs instead?
|
||||
Customize(runArgs interface{}) (*cli.RunArgs, error)
|
||||
//Customize(args interface{}) (*lib.Config, error)
|
||||
//Customize(ctx context.Context, runArgs interface{}) error
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
_ "github.com/purpleidea/mgmt/lang/core/convert"
|
||||
_ "github.com/purpleidea/mgmt/lang/core/datetime"
|
||||
_ "github.com/purpleidea/mgmt/lang/core/deploy"
|
||||
_ "github.com/purpleidea/mgmt/lang/core/embedded"
|
||||
_ "github.com/purpleidea/mgmt/lang/core/example"
|
||||
_ "github.com/purpleidea/mgmt/lang/core/example/nested"
|
||||
_ "github.com/purpleidea/mgmt/lang/core/fmt"
|
||||
|
||||
23
lang/core/embedded/embedded.go
Normal file
23
lang/core/embedded/embedded.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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 coreembedded
|
||||
|
||||
const (
|
||||
// ModuleName is the prefix given to all the functions in this module.
|
||||
ModuleName = "embedded"
|
||||
)
|
||||
12
main.go
12
main.go
@@ -25,6 +25,7 @@ import (
|
||||
|
||||
"github.com/purpleidea/mgmt/cli"
|
||||
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||
"github.com/purpleidea/mgmt/entry"
|
||||
_ "github.com/purpleidea/mgmt/gapi/empty" // import so the gapi registers
|
||||
_ "github.com/purpleidea/mgmt/lang/gapi" // import so the gapi registers
|
||||
_ "github.com/purpleidea/mgmt/yamlgraph" // import so the gapi registers
|
||||
@@ -72,6 +73,17 @@ func main() {
|
||||
},
|
||||
Args: os.Args,
|
||||
}
|
||||
|
||||
// is there an alternate entry point for the cli?
|
||||
if cli, err := entry.Lookup(); err == nil {
|
||||
if err := cli.CLI(context.Background(), data); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
//return // redundant
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := cli.CLI(context.Background(), data); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
|
||||
Reference in New Issue
Block a user