Files
mgmt/entry/entry.go
James Shubin 68ee163eb1 entry, lang: core: embedded: provisioner: Allow more than one entry
This changes the entry API slightly to allow for more than one entry
registered, which makes building, testing and user tooling easier.
2024-04-16 14:11:34 -04:00

360 lines
12 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 <https://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 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
}