diff --git a/Makefile b/Makefile index 6b5d2ef..0ca212f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # build everything at once -all: plugins exe +all: plugins exe test # build the main executable to make configs main: exe @@ -21,8 +21,11 @@ plugins: go build -buildmode=plugin -o lib/syslog.so internal/generator/plugins/syslog/syslog.go go build -buildmode=plugin -o lib/warewulf.so internal/generator/plugins/warewulf/warewulf.go -# remove executable and all plugins +# remove executable and all built plugins clean: rm configurator rm lib/* +# run all of the unit tests +test: + go test ./tests/generate_test.go --tags=all diff --git a/cmd/config.go b/cmd/config.go index 6596988..bd26e7c 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -5,8 +5,8 @@ import ( "github.com/spf13/cobra" - configurator "github.com/OpenCHAMI/configurator/internal" - "github.com/OpenCHAMI/configurator/internal/util" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/util" ) var configCmd = &cobra.Command{ diff --git a/cmd/inspect.go b/cmd/inspect.go index 0a543c3..a2b0adf 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -5,7 +5,7 @@ import ( "maps" "strings" - "github.com/OpenCHAMI/configurator/internal/generator" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/spf13/cobra" ) diff --git a/cmd/root.go b/cmd/root.go index 353523e..cbbfab7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - configurator "github.com/OpenCHAMI/configurator/internal" - "github.com/OpenCHAMI/configurator/internal/util" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/util" "github.com/spf13/cobra" ) diff --git a/internal/generator/plugins/hostfile/hostfile_test.go b/internal/generator/plugins/hostfile/hostfile_test.go deleted file mode 100644 index 06ab7d0..0000000 --- a/internal/generator/plugins/hostfile/hostfile_test.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/internal/schema.go b/internal/schema.go deleted file mode 100644 index dc794a1..0000000 --- a/internal/schema.go +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: implement a way to fetch schemas from node orchestrator -package configurator diff --git a/internal/auth.go b/pkg/auth.go similarity index 100% rename from internal/auth.go rename to pkg/auth.go diff --git a/internal/client.go b/pkg/client.go similarity index 99% rename from internal/client.go rename to pkg/client.go index f5de764..bee15d8 100644 --- a/internal/client.go +++ b/pkg/client.go @@ -12,7 +12,7 @@ import ( "os" "time" - "github.com/OpenCHAMI/configurator/internal/util" + "github.com/OpenCHAMI/configurator/pkg/util" ) type ClientOption func(*SmdClient) diff --git a/internal/config.go b/pkg/config.go similarity index 90% rename from internal/config.go rename to pkg/config.go index ee113dc..10da3c6 100644 --- a/internal/config.go +++ b/pkg/config.go @@ -11,9 +11,9 @@ import ( type Options struct{} type Target struct { - Templates []string `yaml:"templates,omitempty"` - FilePaths []string `yaml:"files,omitempty"` - RunTargets []string `yaml:"targets,omitempty"` + TemplatePaths []string `yaml:"templates,omitempty"` + FilePaths []string `yaml:"files,omitempty"` + RunTargets []string `yaml:"targets,omitempty"` } type Jwks struct { @@ -48,13 +48,13 @@ func NewConfig() Config { }, Targets: map[string]Target{ "dnsmasq": Target{ - Templates: []string{}, + TemplatePaths: []string{}, }, "conman": Target{ - Templates: []string{}, + TemplatePaths: []string{}, }, "warewulf": Target{ - Templates: []string{ + TemplatePaths: []string{ "templates/warewulf/defaults/node.jinja", "templates/warewulf/defaults/provision.jinja", }, diff --git a/internal/configurator.go b/pkg/configurator.go similarity index 100% rename from internal/configurator.go rename to pkg/configurator.go diff --git a/internal/generator/generator.go b/pkg/generator/generator.go similarity index 91% rename from internal/generator/generator.go rename to pkg/generator/generator.go index 1ed6c9d..27a032e 100644 --- a/internal/generator/generator.go +++ b/pkg/generator/generator.go @@ -8,11 +8,10 @@ import ( "path/filepath" "plugin" - configurator "github.com/OpenCHAMI/configurator/internal" - "github.com/OpenCHAMI/configurator/internal/util" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/util" "github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2/exec" - "github.com/sirupsen/logrus" ) type Mappings map[string]any @@ -32,6 +31,7 @@ type Generator interface { type Params struct { Args []string PluginPaths []string + Generators map[string]Generator Target string Verbose bool } @@ -259,14 +259,14 @@ func ApplyTemplateFromFiles(mappings Mappings, paths ...string) (FileMap, error) // 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() +// paths to load all plugins within a directory. Then, each plugin's generator.GenerateWithTarget() // 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) (FileMap, error) { +func GenerateWithTarget(config *configurator.Config, params Params) (FileMap, error) { // load generator plugins to generate configs or to print var ( generators = make(map[string]Generator) @@ -278,12 +278,12 @@ func Generate(config *configurator.Config, params Params) (FileMap, error) { ) ) - // load all plugins from params + // load all plugins from supplied arguments for _, path := range params.PluginPaths { if params.Verbose { fmt.Printf("loading plugins from '%s'\n", path) } - gens, err := LoadPlugins(path) + plugins, err := LoadPlugins(path) if err != nil { fmt.Printf("failed to load plugins: %v\n", err) err = nil @@ -291,9 +291,12 @@ func Generate(config *configurator.Config, params Params) (FileMap, error) { } // add loaded generator plugins to set - maps.Copy(generators, gens) + maps.Copy(generators, plugins) } + // copy all generators supplied from arguments + maps.Copy(generators, params.Generators) + // show available targets then exit if len(params.Args) == 0 && params.Target == "" { for g := range generators { @@ -302,19 +305,14 @@ func Generate(config *configurator.Config, params Params) (FileMap, error) { return nil, nil } - if params.Target == "" { - logrus.Errorf("no target supplied (--target name)") - } else { - // run the generator plugin from target passed - gen := generators[params.Target] - if gen == nil { - return nil, fmt.Errorf("invalid generator target (%s)", params.Target) - } - return gen.Generate( - config, - WithTarget(gen.GetName()), - WithClient(client), - ) + // run the generator plugin from target passed + gen := generators[params.Target] + if gen == nil { + return nil, fmt.Errorf("invalid generator target (%s)", params.Target) } - return nil, fmt.Errorf("an unknown error has occurred") + return gen.Generate( + config, + WithTarget(gen.GetName()), + WithClient(client), + ) } diff --git a/internal/generator/plugins/conman/conman.go b/pkg/generator/plugins/conman/conman.go similarity index 91% rename from internal/generator/plugins/conman/conman.go rename to pkg/generator/plugins/conman/conman.go index b324238..6a14f89 100644 --- a/internal/generator/plugins/conman/conman.go +++ b/pkg/generator/plugins/conman/conman.go @@ -3,9 +3,9 @@ package main import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/internal" - "github.com/OpenCHAMI/configurator/internal/generator" - "github.com/OpenCHAMI/configurator/internal/util" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" ) type Conman struct{} @@ -62,7 +62,7 @@ func (g *Conman) Generate(config *configurator.Config, opts ...util.Option) (gen "plugin_description": g.GetDescription(), "server_opts": "", "global_opts": "", - }, target.Templates...) + }, target.TemplatePaths...) } var Generator Conman diff --git a/internal/generator/plugins/coredhcp/coredhcp.go b/pkg/generator/plugins/coredhcp/coredhcp.go similarity index 100% rename from internal/generator/plugins/coredhcp/coredhcp.go rename to pkg/generator/plugins/coredhcp/coredhcp.go diff --git a/internal/generator/plugins/dhcpd/dhcpd.go b/pkg/generator/plugins/dhcpd/dhcpd.go similarity index 100% rename from internal/generator/plugins/dhcpd/dhcpd.go rename to pkg/generator/plugins/dhcpd/dhcpd.go diff --git a/internal/generator/plugins/dnsmasq/dnsmasq.go b/pkg/generator/plugins/dnsmasq/dnsmasq.go similarity index 91% rename from internal/generator/plugins/dnsmasq/dnsmasq.go rename to pkg/generator/plugins/dnsmasq/dnsmasq.go index 6483fd1..9150009 100644 --- a/internal/generator/plugins/dnsmasq/dnsmasq.go +++ b/pkg/generator/plugins/dnsmasq/dnsmasq.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - configurator "github.com/OpenCHAMI/configurator/internal" - "github.com/OpenCHAMI/configurator/internal/generator" - "github.com/OpenCHAMI/configurator/internal/util" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" ) type DnsMasq struct{} @@ -58,7 +58,7 @@ func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) (ge // print message if verbose param found if verbose, ok := params["verbose"].(bool); ok { if verbose { - fmt.Printf("template: \n%s\nethernet interfaces found: %v\n", strings.Join(target.Templates, "\n\t"), len(eths)) + fmt.Printf("template: \n%s\nethernet interfaces found: %v\n", strings.Join(target.TemplatePaths, "\n\t"), len(eths)) } } @@ -79,7 +79,7 @@ func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) (ge "plugin_version": g.GetVersion(), "plugin_description": g.GetDescription(), "dhcp-hosts": output, - }, target.Templates...) + }, target.TemplatePaths...) } var Generator DnsMasq diff --git a/internal/generator/plugins/example/example.go b/pkg/generator/plugins/example/example.go similarity index 100% rename from internal/generator/plugins/example/example.go rename to pkg/generator/plugins/example/example.go diff --git a/internal/generator/plugins/hostfile/hostfile.go b/pkg/generator/plugins/hostfile/hostfile.go similarity index 100% rename from internal/generator/plugins/hostfile/hostfile.go rename to pkg/generator/plugins/hostfile/hostfile.go diff --git a/internal/generator/plugins/powerman/powerman.go b/pkg/generator/plugins/powerman/powerman.go similarity index 100% rename from internal/generator/plugins/powerman/powerman.go rename to pkg/generator/plugins/powerman/powerman.go diff --git a/internal/generator/plugins/syslog/syslog.go b/pkg/generator/plugins/syslog/syslog.go similarity index 100% rename from internal/generator/plugins/syslog/syslog.go rename to pkg/generator/plugins/syslog/syslog.go diff --git a/internal/generator/plugins/warewulf/warewulf.go b/pkg/generator/plugins/warewulf/warewulf.go similarity index 90% rename from internal/generator/plugins/warewulf/warewulf.go rename to pkg/generator/plugins/warewulf/warewulf.go index f9646b8..8f40d7a 100644 --- a/internal/generator/plugins/warewulf/warewulf.go +++ b/pkg/generator/plugins/warewulf/warewulf.go @@ -5,9 +5,9 @@ import ( "maps" "strings" - configurator "github.com/OpenCHAMI/configurator/internal" - "github.com/OpenCHAMI/configurator/internal/generator" - "github.com/OpenCHAMI/configurator/internal/util" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" ) type Warewulf struct{} @@ -30,7 +30,7 @@ func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (g client = generator.GetClient(params) targetKey = params["target"].(string) target = config.Targets[targetKey] - outputs = make(generator.FileMap, len(target.FilePaths)+len(target.Templates)) + outputs = make(generator.FileMap, len(target.FilePaths)+len(target.TemplatePaths)) ) // check if our client is included and is valid @@ -55,7 +55,7 @@ func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (g // print message if verbose param found if verbose, ok := params["verbose"].(bool); ok { if verbose { - fmt.Printf("template: \n%s\n ethernet interfaces found: %v\n", strings.Join(target.Templates, "\n\t"), len(eths)) + fmt.Printf("template: \n%s\n ethernet interfaces found: %v\n", strings.Join(target.TemplatePaths, "\n\t"), len(eths)) } } @@ -78,7 +78,7 @@ func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (g } templates, err := generator.ApplyTemplateFromFiles(generator.Mappings{ "node_entries": nodeEntries, - }, target.Templates...) + }, target.TemplatePaths...) if err != nil { return nil, fmt.Errorf("failed to load templates: %v", err) } diff --git a/internal/server/server.go b/pkg/server/server.go similarity index 86% rename from internal/server/server.go rename to pkg/server/server.go index 425a594..1144adc 100644 --- a/internal/server/server.go +++ b/pkg/server/server.go @@ -9,8 +9,8 @@ import ( "net/http" "time" - configurator "github.com/OpenCHAMI/configurator/internal" - "github.com/OpenCHAMI/configurator/internal/generator" + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" "github.com/OpenCHAMI/jwtauth/v5" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -108,6 +108,10 @@ func (s *Server) Serve() error { return s.ListenAndServe() } +func (s *Server) Close() { + +} + // This is the corresponding service function to generate templated files, that // works similarly to the CLI variant. This function takes similiar arguments as // query parameters that are included in the HTTP request URL. @@ -115,14 +119,14 @@ func (s *Server) Generate(w http.ResponseWriter, r *http.Request) { // get all of the expect query URL params and validate s.GeneratorParams.Target = r.URL.Query().Get("target") if s.GeneratorParams.Target == "" { - writeError(w, "no targets supplied") + writeErrorResponse(w, "no targets supplied") return } // generate a new config file from supplied params - outputs, err := generator.Generate(s.Config, s.GeneratorParams) + outputs, err := generator.GenerateWithTarget(s.Config, s.GeneratorParams) if err != nil { - writeError(w, "failed to generate config: %v", err) + writeErrorResponse(w, "failed to generate config: %v", err) return } @@ -130,12 +134,12 @@ func (s *Server) Generate(w http.ResponseWriter, r *http.Request) { tmp := generator.ConvertContentsToString(outputs) b, err := json.Marshal(tmp) if err != nil { - writeError(w, "failed to marshal output: %v", err) + writeErrorResponse(w, "failed to marshal output: %v", err) return } _, err = w.Write(b) if err != nil { - writeError(w, "failed to write response: %v", err) + writeErrorResponse(w, "failed to write response: %v", err) return } } @@ -147,15 +151,15 @@ func (s *Server) Generate(w http.ResponseWriter, r *http.Request) { func (s *Server) ManageTemplates(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("this is not implemented yet")) if err != nil { - writeError(w, "failed to write response: %v", err) + writeErrorResponse(w, "failed to write response: %v", err) return } } // Wrapper function to simplify writting error message responses. This function // is only intended to be used with the service and nothing else. -func writeError(w http.ResponseWriter, format string, a ...any) { +func writeErrorResponse(w http.ResponseWriter, format string, a ...any) error { errmsg := fmt.Sprintf(format, a...) - fmt.Printf(errmsg) w.Write([]byte(errmsg)) + return fmt.Errorf(errmsg) } diff --git a/internal/util/params.go b/pkg/util/params.go similarity index 100% rename from internal/util/params.go rename to pkg/util/params.go diff --git a/internal/util/util.go b/pkg/util/util.go similarity index 100% rename from internal/util/util.go rename to pkg/util/util.go diff --git a/tests/generate_test.go b/tests/generate_test.go new file mode 100644 index 0000000..31e6e04 --- /dev/null +++ b/tests/generate_test.go @@ -0,0 +1,402 @@ +package tests + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "testing" + + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/server" + "github.com/OpenCHAMI/configurator/pkg/util" +) + +// A valid test generator that implements the `Generator` interface. +type TestGenerator struct{} + +func (g *TestGenerator) GetName() string { return "test" } +func (g *TestGenerator) GetVersion() string { return "v1.0.0" } +func (g *TestGenerator) GetDescription() string { + return "This is a plugin created for running tests." +} +func (g *TestGenerator) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { + // Jinja 2 template file + files := [][]byte{ + []byte(` +Name: {{plugin_name}} +Version: {{plugin_version}} +Description: {{plugin_description}} + +This is the first test template file. + `), + []byte(` +This is another testing Jinja 2 template file using {{plugin_name}}. + `), + } + + // apply Jinja templates to file + fileList, err := generator.ApplyTemplates(generator.Mappings{ + "plugin_name": g.GetName(), + "plugin_version": g.GetVersion(), + "plugin_description": g.GetDescription(), + }, files...) + if err != nil { + return nil, fmt.Errorf("failed to apply templates: %v", err) + } + + // make sure we're able to receive certain arguments when passed + params := generator.GetParams(opts...) + if len(params) <= 0 { + return nil, fmt.Errorf("expect at least one params, but found none") + } + + // make sure we have a valid config we can access + if config == nil { + return nil, fmt.Errorf("invalid config (config is nil)") + } + + // make sure we're able to get a valid client as well + client := generator.GetClient(params) + if client == nil { + return nil, fmt.Errorf("invalid client (client is nil)") + } + + // TODO: make sure we can get a target + + // make sure we have the same number of files in file list + if len(files) != len(fileList) { + return nil, fmt.Errorf("file list output count is not the same as the input") + } + + // convert file list to file map + fileMap := make(generator.FileMap, len(fileList)) + for i, contents := range fileList { + fileMap[fmt.Sprintf("t-%d.txt", i)] = contents + } + + return fileMap, nil +} + +// Test building and loading plugins +func TestPlugin(t *testing.T) { + var ( + testPluginDir = t.TempDir() + testPluginPath = fmt.Sprintf("%s/test-plugin.so", testPluginDir) + testPluginSourcePath = fmt.Sprintf("%s/test-plugin.go", testPluginDir) + testPluginSource = []byte(` +package main + +import ( + "fmt" + + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" +) + +type TestGenerator struct{} + +func (g *TestGenerator) GetName() string { return "test" } +func (g *TestGenerator) GetVersion() string { return "v1.0.0" } +func (g *TestGenerator) GetDescription() string { return "This is a plugin creating for running tests." } +func (g *TestGenerator) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { + return generator.FileMap{"test": []byte("test")}, nil +} +var Generator TestGenerator + `) + ) + + wd, err := os.Getwd() + if err != nil { + t.Errorf("failed to get working directory: %v", err) + } + + // show all paths to make sure we're using the correct ones + fmt.Printf("(TestPlugin) working directory: %v\n", wd) + fmt.Printf("(TestPlugin) plugin directory: %v\n", testPluginDir) + fmt.Printf("(TestPlugin) plugin path: %v\n", testPluginPath) + fmt.Printf("(TestPlugin) plugin source path: %v\n", testPluginSourcePath) + + // make temporary directory to test plugin + err = os.MkdirAll(testPluginDir, os.ModeDir) + if err != nil { + t.Fatalf("failed to make temporary directory: %v", err) + } + + // dump the plugin source code to a file + err = os.WriteFile(testPluginSourcePath, testPluginSource, os.ModePerm) + if err != nil { + t.Fatalf("failed to write test plugin file: %v", err) + } + + // make sure the source file was actually written + fileInfo, err := os.Stat(testPluginSourcePath) + if err != nil { + t.Fatalf("failed to stat path: %v", err) + } + if fileInfo.IsDir() { + t.Fatalf("expected file but found directory") + } + + // change to testing directory to run command + err = os.Chdir(testPluginDir) + if err != nil { + t.Fatalf("failed to 'cd' to temporary directory: %v", err) + } + + // execute command to build the plugin + cmd := exec.Command("go", "build", "-buildmode=plugin", fmt.Sprintf("-o=%s", testPluginPath), testPluginSourcePath) + if output, err := cmd.Output(); err != nil { + t.Fatalf("failed to execute command: %v\n%s", err, string(output)) + } + + // stat the file to confirm that the plugin was built + fileInfo, err = os.Stat(testPluginPath) + if err != nil { + t.Fatalf("failed to stat plugin file: %v", err) + } + if fileInfo.IsDir() { + t.Fatalf("directory file but a file was expected") + } + if fileInfo.Size() <= 0 { + t.Fatal("found an empty file or file with size of 0 bytes") + } + + // test loading plugins both individually and in a dir + gen, err := generator.LoadPlugin(testPluginSourcePath) + if err != nil { + t.Fatalf("failed to load the test plugin: %v", err) + } + + // test that we have all expected methods with type assertions + if _, ok := gen.(interface { + GetName() string + GetVersion() string + GetDescription() string + Generate(*configurator.Config, ...util.Option) (generator.FileMap, error) + }); !ok { + t.Error("plugin does not implement all of the generator interface") + } + + // test loading plugins from a directory (should just load a single one) + gens, err := generator.LoadPlugins(testPluginDir) + if err != nil { + t.Fatalf("failed to load plugins in '%s': %v", testPluginDir, err) + } + + // test all of the plugins loaded from a directory (should expect same result as above) + for _, gen := range gens { + if _, ok := gen.(interface { + GetName() string + GetVersion() string + GetDescription() string + Generate(*configurator.Config, ...util.Option) (generator.FileMap, error) + }); !ok { + t.Error("plugin does not implement all of the generator interface") + } + } + +} + +// Test that expects to fail with a specific error using a partially +// implemented generator. The purpose of this test is to make sure we're +// seeing the correct error that we would expect in these situations. +// The errors should be something like: +// - no symbol: "failed to look up symbol at path" +// - invalid symbol: "failed to load the correct symbol type at path" +func TestPluginWithInvalidOrNoSymbol(t *testing.T) { + var ( + testPluginDir = t.TempDir() + testPluginPath = fmt.Sprintf("%s/invalid-plugin.so", testPluginDir) + testPluginSourcePath = fmt.Sprintf("%s/invalid-plugin.go", testPluginDir) + testPluginSource = []byte(` +package main + +import ( + "fmt" + + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/util" +) + +// An invalid generator that does not or partially implements +// the "Generator" interface. +type InvalidGenerator struct{} +var Generator TestGenerator + `) + ) + + wd, err := os.Getwd() + if err != nil { + t.Errorf("failed to get working directory: %v", err) + } + // show all paths to make sure we're using the correct ones + fmt.Printf("(TestPluginWithInvalidOrNoSymbol) working directory: %v\n", wd) + fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin directory: %v\n", testPluginDir) + fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin path: %v\n", testPluginPath) + fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin source path: %v\n", testPluginSourcePath) + + // make temporary directory to test plugin + err = os.MkdirAll(testPluginDir, os.ModeDir) + if err != nil { + t.Fatalf("failed to make temporary directory: %v", err) + } + + // dump the plugin source code to a file + err = os.WriteFile(testPluginSourcePath, testPluginSource, os.ModePerm) + if err != nil { + t.Fatalf("failed to write test plugin file: %v", err) + } + + // make sure the source file was actually written + fileInfo, err := os.Stat(testPluginSourcePath) + if err != nil { + t.Fatalf("failed to stat path: %v", err) + } + if fileInfo.IsDir() { + t.Fatalf("expected file but found directory") + } + + // change to testing directory to run command + err = os.Chdir(testPluginDir) + if err != nil { + t.Fatalf("failed to 'cd' to temporary directory: %v", err) + } + + // execute command to build the plugin + cmd := exec.Command("go", "build", "-buildmode=plugin", fmt.Sprintf("-o=%s", testPluginPath), testPluginSourcePath) + if output, err := cmd.Output(); err != nil { + t.Fatalf("failed to execute command: %v\n%s", err, string(output)) + } + + // stat the file to confirm that the plugin was built + fileInfo, err = os.Stat(testPluginPath) + if err != nil { + t.Fatalf("failed to stat plugin file: %v", err) + } + if fileInfo.IsDir() { + t.Fatalf("directory file but a file was expected") + } + if fileInfo.Size() <= 0 { + t.Fatal("found an empty file or file with size of 0 bytes") + } + + // try and load plugin, but expect specific error + _, err = generator.LoadPlugin(testPluginSourcePath) + if err == nil { + t.Fatalf("expected an error, but returned nil") + } +} + +// Test that expects to successfully "generate" a file using the built-in +// example plugin with no fetching. +// +// NOTE: Normally we would dynamically load a generator from a plugin, but +// we're not doing it here since that's not what is being tested. +func TestGenerateExample(t *testing.T) { + var ( + config = configurator.NewConfig() + client = configurator.NewSmdClient() + gen = TestGenerator{} + ) + + // make sure our generator returns expected strings + t.Run("properties", func(t *testing.T) { + if gen.GetName() != "test" { + t.Error("test generator return unexpected name") + } + if gen.GetVersion() != "v1.0.0" { + t.Error("test generator return unexpected version") + } + if gen.GetDescription() != "This is a plugin creating for running tests." { + t.Error("test generator return unexpected description") + } + }) + + // try to generate a file with templating applied + fileMap, err := gen.Generate( + &config, + generator.WithTarget("test"), + generator.WithClient(client), + ) + if err != nil { + t.Fatalf("failed to generate file: %v", err) + } + + // test for 2 expected files to be generated in the output (hint: check the + // TestGenerator.Generate implementation) + if len(fileMap) != 2 { + t.Error("expected 2 files in generated output") + } +} + +// Test that expects to successfully "generate" a file using the built-in +// example plugin but by making a HTTP request to a service instance instead. +// +// NOTE: This test uses the default server settings to run. Also, no need to +// try and load the plugin from a lib here either. +func TestGenerateExampleWithServer(t *testing.T) { + var ( + config = configurator.NewConfig() + client = configurator.NewSmdClient() + gen = TestGenerator{} + headers = make(map[string]string, 0) + ) + + // NOTE: Currently, the server needs a config to know where to get load plugins, + // and how to handle targets/templates. This will be simplified in the future to + // decoupled the server from required a config altogether. + config.Targets["test"] = configurator.Target{ + TemplatePaths: []string{}, + FilePaths: []string{}, + } + + // show which targets are availabe in the config + fmt.Printf("targets:\n") + for target, _ := range config.Targets { + fmt.Printf("\t- %s\n", target) + } + + // create new server, add test generator, and start in background + server := server.New(&config) + server.GeneratorParams.Generators = map[string]generator.Generator{ + "test": &gen, + } + go server.Serve() + + // make request to server to generate a file + res, b, err := util.MakeRequest("http://127.0.0.1:3334/generate?target=test", http.MethodGet, nil, headers) + if err != nil { + t.Fatalf("failed to make request: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Fatalf("expect status code 200 from response but received %d instead", res.StatusCode) + } + + // test for specific output from request + // + // NOTE: we don't actually use the config in this plugin implementation, + // but we do check that a valid config was passed. + fileMap, err := gen.Generate( + &config, + generator.WithClient(client), + ) + if err != nil { + t.Fatalf("failed to generate file: %v", err) + } + for path, contents := range fileMap { + tmp := make(map[string]string, 1) + err := json.Unmarshal(b, &tmp) + if err != nil { + t.Errorf("failed to unmarshal response: %v", err) + continue + } + if string(contents) != string(tmp[path]) { + t.Fatalf("response does not match expected output...\nexpected:%s\noutput:%s", string(contents), string(tmp[path])) + } + } +}