Files
mgmt/entry/entry.go
James Shubin d30ff6cfae legal: Remove year
Instead of constantly making these updates, let's just remove the year
since things are stored in git anyways, and this is not an actual modern
legal risk anymore.
2025-01-26 16:24:51 -05:00

360 lines
12 KiB
Go

// Mgmt
// Copyright (C) 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
}