diff --git a/cmd/fetch.go b/cmd/fetch.go index 956e3bc..4b9913b 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -55,7 +55,7 @@ func init() { fetchCmd.Flags().IntVar(&remotePort, "port", 3334, "set the remote configurator port") fetchCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make") fetchCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets") - fetchCmd.Flags().StringVar(&accessToken, "access-token", "o", "", "set the output path for config targets") + fetchCmd.Flags().StringVar(&accessToken, "access-token", "o", "set the output path for config targets") rootCmd.AddCommand(fetchCmd) } diff --git a/cmd/generate.go b/cmd/generate.go index aaf4a1c..ed2ea3a 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -61,12 +61,12 @@ var generateCmd = &cobra.Command{ fmt.Printf("%v\n", string(b)) } - RunTargets(targets...) + RunTargets(&config, args, targets...) }, } -func RunTargets(config *configurator.Config, targets ...string) { +func RunTargets(config *configurator.Config, args []string, targets ...string) { // generate config with each supplied target for _, target := range targets { params := generator.Params{ @@ -75,13 +75,13 @@ func RunTargets(config *configurator.Config, targets ...string) { Target: target, Verbose: verbose, } - outputBytes, err := generator.Generate(&config, params) + outputBytes, err := generator.Generate(config, params) if err != nil { fmt.Printf("failed to generate config: %v\n", err) os.Exit(1) } - outputMap := util.ConvertMapOutput(outputBytes) + outputMap := generator.ConvertContentsToString(outputBytes) // if we have more than one target and output is set, create configs in directory var ( @@ -129,10 +129,10 @@ func RunTargets(config *configurator.Config, targets ...string) { } // remove any targets that are the same as current to prevent infinite loop - nextTargets := util.CopyIf(config.Targets[targets].Targets, func(t T) bool { return t != target }) + nextTargets := util.CopyIf(config.Targets[target].RunTargets, func(t string) bool { return t != target }) // ...then, run any other targets that the current target has - RunTargets(config, nextTargets...) + RunTargets(config, args, nextTargets...) } } diff --git a/cmd/inspect.go b/cmd/inspect.go index 4ca4daf..0a543c3 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -10,6 +10,7 @@ import ( ) var ( + byTarget bool pluginDirs []string generators map[string]generator.Generator ) @@ -17,6 +18,7 @@ var ( var inspectCmd = &cobra.Command{ Use: "inspect", Short: "Inspect generator plugin information", + Long: "The 'inspect' sub-command takes a list of directories and prints all found plugin information.", Run: func(cmd *cobra.Command, args []string) { // load specific plugins from positional args generators = make(map[string]generator.Generator) @@ -26,6 +28,10 @@ var inspectCmd = &cobra.Command{ fmt.Printf("failed to load plugin at path '%s': %v\n", path, err) continue } + // path is directory, so no plugin is loaded, but no error was returned + if gen == nil { + continue + } generators[path] = gen } @@ -59,5 +65,6 @@ var inspectCmd = &cobra.Command{ } func init() { + inspectCmd.Flags().BoolVar(&byTarget, "by-target", false, "set whether to ") rootCmd.AddCommand(inspectCmd) } diff --git a/internal/client.go b/internal/client.go index 1226095..f351540 100644 --- a/internal/client.go +++ b/internal/client.go @@ -15,6 +15,7 @@ import ( "github.com/OpenCHAMI/configurator/internal/util" ) +type ClientOption func(*SmdClient) type SmdClient struct { http.Client `json:"-"` Host string `yaml:"host"` @@ -22,10 +23,6 @@ type SmdClient struct { AccessToken string `yaml:"access-token"` } -type Params = map[string]any -type Option func(Params) -type ClientOption func(*SmdClient) - func NewSmdClient(opts ...ClientOption) SmdClient { client := SmdClient{} for _, opt := range opts { @@ -80,14 +77,14 @@ func WithSecureTLS(certPath string) ClientOption { return WithCertPool(certPool) } -func WithVerbosity() Option { +func WithVerbosity() util.Option { return func(p util.Params) { p["verbose"] = true } } -func NewParams() Params { - return Params{ +func NewParams() util.Params { + return util.Params{ "verbose": false, } } diff --git a/internal/config.go b/internal/config.go index df58cc5..ee113dc 100644 --- a/internal/config.go +++ b/internal/config.go @@ -38,6 +38,7 @@ type Config struct { Options Options `yaml:"options"` } +// Creates a new config with default parameters. func NewConfig() Config { return Config{ Version: "", diff --git a/internal/generator/generator.go b/internal/generator/generator.go index 6687634..de68551 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -15,8 +15,11 @@ import ( "github.com/sirupsen/logrus" ) -type Mappings = map[string]any -type Files = map[string][]byte +type Mappings map[string]any +type Files map[string][]byte + +// Generator interface used to define how files are created. Plugins can +// be created entirely independent of the main driver program. type Generator interface { GetName() string GetVersion() string @@ -24,6 +27,7 @@ type Generator interface { Generate(config *configurator.Config, opts ...util.Option) (Files, error) } +// Params defined and used by the "generate" subcommand. type Params struct { Args []string PluginPaths []string @@ -31,139 +35,15 @@ type Params struct { Verbose bool } -func LoadPlugin(path string) (Generator, error) { - p, err := plugin.Open(path) - if err != nil { - return nil, fmt.Errorf("failed to load plugin: %v", err) +func ConvertContentsToString(f Files) map[string]string { + n := make(map[string]string, len(f)) + for k, v := range f { + n[k] = string(v) } - - // load the "Generator" symbol from plugin - symbol, err := p.Lookup("Generator") - if err != nil { - return nil, fmt.Errorf("failed to look up symbol at path '%s': %v", path, err) - } - - // assert that the plugin loaded has a valid generator - gen, ok := symbol.(Generator) - if !ok { - return nil, fmt.Errorf("failed to load the correct symbol type at path '%s'", path) - } - return gen, nil -} - -func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, error) { - // check if verbose option is supplied - var ( - gens = make(map[string]Generator) - params = util.GetParams(opts...) - ) - - items, _ := os.ReadDir(dirpath) - for _, item := range items { - if item.IsDir() { - subitems, _ := os.ReadDir(item.Name()) - for _, subitem := range subitems { - if !subitem.IsDir() { - gen, err := LoadPlugin(subitem.Name()) - if err != nil { - fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err) - continue - } - if verbose, ok := params["verbose"].(bool); ok { - if verbose { - fmt.Printf("-- found plugin '%s'\n", item.Name()) - } - } - gens[gen.GetName()] = gen - } - } - } else { - gen, err := LoadPlugin(dirpath + item.Name()) - if err != nil { - fmt.Printf("failed to load generator: %v\n", err) - continue - } - if verbose, ok := params["verbose"].(bool); ok { - if verbose { - fmt.Printf("-- found plugin '%s'\n", dirpath+item.Name()) - } - } - gens[gen.GetName()] = gen - } - } - - return gens, nil -} - -func WithTarget(target string) util.Option { - return func(p util.Params) { - if p != nil { - p["target"] = target - } - } -} - -func WithType(_type string) util.Option { - return func(p util.Params) { - if p != nil { - p["type"] = _type - } - } -} - -func WithClient(client configurator.SmdClient) util.Option { - return func(p util.Params) { - p["client"] = client - } -} - -func WithOption(key string, value any) util.Option { - return func(p util.Params) { - p[key] = value - } -} - -// Helper function to get client in generator plugins. -func GetClient(params util.Params) *configurator.SmdClient { - return util.Get[configurator.SmdClient](params, "client") -} - -func GetTarget(config *configurator.Config, key string) configurator.Target { - return config.Targets[key] -} - -func GetParams(opts ...util.Option) util.Params { - params := util.Params{} - for _, opt := range opts { - opt(params) - } - return params -} - -func ApplyTemplates(mappings map[string]any, paths ...string) (Files, error) { - var ( - data = exec.NewContext(mappings) - outputs = Files{} - ) - - for _, path := range paths { - // load jinja template from file - t, err := gonja.FromFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read template from file: %v", err) - } - - // execute/render jinja template - b := bytes.Buffer{} - if err = t.Execute(&b, data); err != nil { - return nil, fmt.Errorf("failed to execute: %v", err) - } - outputs[path] = b.Bytes() - } - - return outputs, nil + return n } +// Loads files without applying any Jinja 2 templating. func LoadFiles(paths ...string) (Files, error) { var outputs = Files{} for _, path := range paths { @@ -193,6 +73,167 @@ func LoadFiles(paths ...string) (Files, error) { return outputs, nil } +// Loads a single generator plugin given a single file path. +func LoadPlugin(path string) (Generator, error) { + // skip loading plugin if path is a directory with no error + if isDir, err := util.IsDirectory(path); err == nil && isDir { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("failed to test if path is directory: %v", err) + } + + // try and open the plugin + p, err := plugin.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open plugin: %v", err) + } + + // load the "Generator" symbol from plugin + symbol, err := p.Lookup("Generator") + if err != nil { + return nil, fmt.Errorf("failed to look up symbol at path '%s': %v", path, err) + } + + // assert that the plugin loaded has a valid generator + gen, ok := symbol.(Generator) + if !ok { + return nil, fmt.Errorf("failed to load the correct symbol type at path '%s'", path) + } + return gen, nil +} + +// Loads all generator plugins in a given directory. +// +// Returns a map of generators. Each generator can be accessed by the name +// returned by the generator.GetName() implemented. +func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, error) { + // check if verbose option is supplied + var ( + gens = make(map[string]Generator) + params = util.GetParams(opts...) + ) + + items, _ := os.ReadDir(dirpath) + for _, item := range items { + if item.IsDir() { + subitems, _ := os.ReadDir(item.Name()) + for _, subitem := range subitems { + if !subitem.IsDir() { + gen, err := LoadPlugin(subitem.Name()) + if err != nil { + fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err) + continue + } + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("-- found plugin '%s'\n", item.Name()) + } + } + gens[gen.GetName()] = gen + } + } + } else { + gen, err := LoadPlugin(dirpath + item.Name()) + if err != nil { + fmt.Printf("failed to load plugin: %v\n", err) + continue + } + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("-- found plugin '%s'\n", dirpath+item.Name()) + } + } + gens[gen.GetName()] = gen + } + } + + return gens, nil +} + +// Option to specify "target" in parameter map. This is used to set which generator +// to use to generate a config file. +func WithTarget(target string) util.Option { + return func(p util.Params) { + if p != nil { + p["target"] = target + } + } +} + +// Option to specify "type" in parameter map. This is not currently used. +func WithType(_type string) util.Option { + return func(p util.Params) { + if p != nil { + p["type"] = _type + } + } +} + +// Option to a specific client to include in implementing plugin generator.Generate(). +// +// NOTE: This may be changed to pass some kind of client interface as an argument in +// the future instead. +func WithClient(client configurator.SmdClient) util.Option { + return func(p util.Params) { + p["client"] = client + } +} + +// Helper function to get client in generator.Generate() plugin implementations. +func GetClient(params util.Params) *configurator.SmdClient { + return util.Get[configurator.SmdClient](params, "client") +} + +// Helper function to get the target in generator.Generate() plugin implementations. +func GetTarget(config *configurator.Config, key string) configurator.Target { + return config.Targets[key] +} + +// Helper function to load all options set with With*() into parameter map. +func GetParams(opts ...util.Option) util.Params { + params := util.Params{} + for _, opt := range opts { + opt(params) + } + return params +} + +// Wrapper function to slightly abstract away some of the nuances with using gonja +// into a single function call. This function is *mostly* for convenience and +// simplication. +func ApplyTemplates(mappings map[string]any, paths ...string) (Files, error) { + var ( + data = exec.NewContext(mappings) + outputs = Files{} + ) + + for _, path := range paths { + // load jinja template from file + t, err := gonja.FromFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read template from file: %v", err) + } + + // execute/render jinja template + b := bytes.Buffer{} + if err = t.Execute(&b, data); err != nil { + return nil, fmt.Errorf("failed to execute: %v", err) + } + outputs[path] = b.Bytes() + } + + return outputs, nil +} + +// Main function to generate a collection of files as a map with the path as the key and +// the contents of the file as the value. This function currently expects a list of plugin +// paths to load all plugins within a directory. Then, each plugin's generator.Generate() +// function is called for each target specified. +// +// This function is the corresponding implementation for the "generate" CLI subcommand. +// It is also call when running the configurator as a service with the "/generate" route. +// +// TODO: Separate loading plugins so we can load them once when running as a service. func Generate(config *configurator.Config, params Params) (Files, error) { // load generator plugins to generate configs or to print var ( diff --git a/internal/server/server.go b/internal/server/server.go index 5c16ca6..af1520d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -33,8 +33,9 @@ type Server struct { TokenAuth *jwtauth.JWTAuth } -func New() *Server { +func New(config *configurator.Config) *Server { return &Server{ + Config: config, Server: &http.Server{ Addr: "localhost:3334", }, diff --git a/internal/util/params.go b/internal/util/params.go index 2417083..6b342cd 100644 --- a/internal/util/params.go +++ b/internal/util/params.go @@ -6,9 +6,11 @@ import ( "golang.org/x/exp/maps" ) -type Params = map[string]any +// Params are accessible in generator.Generate(). +type Params map[string]any type Option func(Params) +// Extract all parameters from the options passed as map[string]any. func GetParams(opts ...Option) Params { params := Params{} for _, opt := range opts { @@ -17,7 +19,8 @@ func GetParams(opts ...Option) Params { return params } -func OptionExists(params Params, opt string) bool { +// Test if an option is present in params +func (p *Params) OptionExists(params Params, opt string) bool { var k []string = maps.Keys(params) return slices.Contains(k, opt) } diff --git a/internal/util/util.go b/internal/util/util.go index 8090ac4..58ddb47 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -11,6 +11,7 @@ import ( "strings" ) +// Wrapper function to simplify checking if a path exists. func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { @@ -22,6 +23,19 @@ func PathExists(path string) (bool, error) { return false, err } +func IsDirectory(path string) (bool, error) { + // This returns an *os.FileInfo type + fileInfo, err := os.Stat(path) + if err != nil { + return false, fmt.Errorf("failed to stat path: %v", err) + } + + // IsDir is short for fileInfo.Mode().IsDir() + return fileInfo.IsDir(), nil +} + +// Wrapper function to confine making a HTTP request into a single function +// instead of multiple. func MakeRequest(url string, httpMethod string, body []byte, headers map[string]string) (*http.Response, []byte, error) { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} req, err := http.NewRequest(httpMethod, url, bytes.NewBuffer(body)) @@ -44,14 +58,9 @@ func MakeRequest(url string, httpMethod string, body []byte, headers map[string] return res, b, err } -func ConvertMapOutput(m map[string][]byte) map[string]string { - n := make(map[string]string, len(m)) - for k, v := range m { - n[k] = string(v) - } - return n -} - +// Returns the git commit string by executing command. +// NOTE: This currently requires git to be installed. +// TODO: Change how this is done to not require executing a command. func GitCommit() string { c := exec.Command("git", "rev-parse", "HEAD") stdout, err := c.Output() @@ -62,13 +71,15 @@ func GitCommit() string { return strings.TrimRight(string(stdout), "\n") } -// NOTE: would it be better to use slices.DeleteFunc instead +// General function to remove element by a given index. +// NOTE: would it be better to use slices.DeleteFunc instead? func RemoveIndex[T comparable](s []T, index int) []T { ret := make([]T, 0) ret = append(ret, s[:index]...) return append(ret, s[index+1:]...) } +// General function to copy elements from slice if condition is true. func CopyIf[T comparable](s []T, condition func(t T) bool) []T { var f = make([]T, 0) for _, e := range s {