// Mgmt // Copyright (C) 2013-2024+ James Shubin and the project contributors // Written by James Shubin and the project contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // Additional permission under GNU GPL version 3 section 7 // // If you modify this program, or any covered work, by linking or combining it // with embedded mcl code and modules (and that the embedded mcl code and // modules which link with this program, contain a copy of their source code in // the authoritative form) containing parts covered by the terms of any other // license, the licensors of this program grant you additional permission to // convey the resulting work. Furthermore, the licensors of this program grant // the original author, James Shubin, additional permission to update this // additional permission if he deems it necessary to achieve the goals of this // additional permission. // 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 map of "registered" entry points that we built with. // You can have more than one registered. Each will appear under the top-level // binary name as a subcommand. var registeredData = make(map[string]*Data) // must initialize // Register takes a data struct and stores a reference to it in our entry system // along with a name. Future lookups to that name will pull out that data. // 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 functions should run. Multiple entry data struct can be // registered at a time. There is no matching Unregister function at this time. func Register(name string, data *Data) { if _, exists := registeredData[name]; exists { panic(fmt.Sprintf("an entry named %s is already registered", name)) } if err := data.Validate(); err != nil { panic(err) } registeredData[name] = 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 // Debug represents if we're running in debug mode or not. Debug bool // Logf is a logger which should be used. Logf func(format string, v ...interface{}) // 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 takes a name and returns the runner that implements the complex // plumbing to kick off the run. If one has not been registered under that name, // then this will error. func Lookup(name string) (*Runner, error) { data, exists := registeredData[name] if !exists { return nil, fmt.Errorf("could not lookup entry named: %s", name) } if data == nil { return nil, fmt.Errorf("registered entry data was nil") } return &Runner{ data: data, // *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 } // Name returns the name of the program, which should match the argv[1] of what // we want to use to call this from the top-level main. func (obj *Runner) Name() string { return obj.data.Program } // 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 //debug := data.Flags.Debug // this one comes from main debug := obj.data.Debug // this one comes from entry logf := func(format string, v ...interface{}) { //data.Flags.Logf(obj.data.Program+": "+format, v...) obj.data.Logf(obj.data.Program+": "+format, v...) } if obj.data.Custom != nil { if x, ok := obj.data.Custom.(Initable); ok { init := &Init{ Data: data, Debug: debug, Logf: logf, // TODO: add more? } if err := x.Init(init); err != nil { return errwrap.Wrapf(err, "can't init custom struct") } } // 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 } // Init is some data and handles to pass in. type Init struct { // Data is the original data that we get from the core compilation. Data *cliUtil.Data Debug bool Logf func(format string, v ...interface{}) } // Initable lets us have a way to pass in some data and handles if the struct // wants them. Implementing this is optional. type Initable interface { Customizable // Init passes in some data and handles. Init(*Init) error } // 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 }