diff --git a/cli/cli.go b/cli/cli.go index 87d06326..c722a9cf 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -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 diff --git a/entry/entry.go b/entry/entry.go new file mode 100644 index 00000000..c469a078 --- /dev/null +++ b/entry/entry.go @@ -0,0 +1,297 @@ +// Mgmt +// Copyright (C) 2013-2024+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Package 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 +} diff --git a/lang/core/core.go b/lang/core/core.go index 7360bd99..c11b9772 100644 --- a/lang/core/core.go +++ b/lang/core/core.go @@ -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" diff --git a/lang/core/embedded/embedded.go b/lang/core/embedded/embedded.go new file mode 100644 index 00000000..36eb267b --- /dev/null +++ b/lang/core/embedded/embedded.go @@ -0,0 +1,23 @@ +// Mgmt +// Copyright (C) 2013-2024+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package coreembedded + +const ( + // ModuleName is the prefix given to all the functions in this module. + ModuleName = "embedded" +) diff --git a/main.go b/main.go index a7739ba6..1d563201 100644 --- a/main.go +++ b/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)