With the recent merging of embedded package imports and the entry CLI package, it is now possible for users to build in mcl code into a single binary. This additional permission makes it explicitly clear that this is permitted to make it easier for those users. The condition is phrased so that the terms can be "patched" by the original author if it's necessary for the project. For example, if the name of the language (mcl) changes, has a differently named new version, someone finds a phrasing improvement or a legal loophole, or for some other reasonable circumstance. Now go write some beautiful embedded tools!
310 lines
11 KiB
Go
310 lines
11 KiB
Go
// 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/>.
|
|
//
|
|
// 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 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
|
|
}
|