diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c68b3..51332f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +[0.2.0] + +- Major rewrite of entire code base + [0.1.0] - Initial prerelease of configurator \ No newline at end of file diff --git a/cmd/config.go b/cmd/config.go deleted file mode 100644 index f56c34c..0000000 --- a/cmd/config.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" -) - -var configCmd = &cobra.Command{ - Use: "config", - Short: "Create a new default config file", - Run: func(cmd *cobra.Command, args []string) { - // create a new config at all args (paths) - // - // TODO: change this to only take a single arg since more - // than one arg is *maybe* a mistake - for _, path := range args { - // check and make sure something doesn't exist first - if exists, err := util.PathExists(path); exists || err != nil { - log.Error().Err(err).Msg("file or directory exists") - continue - } - config.SaveDefault(path) - } - }, -} - -func init() { - rootCmd.AddCommand(configCmd) -} diff --git a/cmd/download.go b/cmd/download.go new file mode 100644 index 0000000..0160d4f --- /dev/null +++ b/cmd/download.go @@ -0,0 +1,7 @@ +package cmd + +import "github.com/spf13/cobra" + +var downloadCmd = cobra.Command{ + Use: "download", +} diff --git a/cmd/fetch.go b/cmd/fetch.go deleted file mode 100644 index 0b105a7..0000000 --- a/cmd/fetch.go +++ /dev/null @@ -1,77 +0,0 @@ -//go:build client || all -// +build client all - -package cmd - -import ( - "fmt" - "net/http" - "os" - - "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var fetchCmd = &cobra.Command{ - Use: "fetch", - Short: "Fetch a config file from a remote instance of configurator", - Long: "This command is simplified to make a HTTP request to the a configurator service.", - Run: func(cmd *cobra.Command, args []string) { - // make sure a host is set - if remoteHost == "" { - log.Error().Msg("no '--host' argument set") - return - } - - // check if we actually have any targets to run - if len(targets) <= 0 { - log.Error().Msg("must specify a target") - os.Exit(1) - } - - // check to see if an access token is available from env - if conf.AccessToken == "" { - // check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead - accessToken := os.Getenv("ACCESS_TOKEN") - if accessToken != "" { - conf.AccessToken = accessToken - } else { - // TODO: try and fetch token first if it is needed - if verbose { - log.Warn().Msg("No token found. Attempting to generate config without one...") - } - } - } - - // add the "Authorization" header if an access token is supplied - headers := map[string]string{} - if accessToken != "" { - headers["Authorization"] = "Bearer " + accessToken - } - - for _, target := range targets { - // make a request for each target - url := fmt.Sprintf("%s/generate?target=%s", remoteHost, target) - res, body, err := util.MakeRequest(url, http.MethodGet, nil, headers) - if err != nil { - log.Error().Err(err).Msg("failed to fetch files") - return - } - // handle getting other error codes other than a 200 - if res != nil { - // NOTE: the server responses are already marshaled to JSON - fmt.Print(string(body)) - } - } - }, -} - -func init() { - fetchCmd.Flags().StringVar(&remoteHost, "host", "", "set the remote configurator host and 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") - - rootCmd.AddCommand(fetchCmd) -} diff --git a/cmd/generate.go b/cmd/generate.go deleted file mode 100644 index 578c9f0..0000000 --- a/cmd/generate.go +++ /dev/null @@ -1,212 +0,0 @@ -//go:build client || all -// +build client all - -package cmd - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/generator" - "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var ( - tokenFetchRetries int - templatePaths []string - pluginPath string - useCompression bool -) - -var generateCmd = &cobra.Command{ - Use: "generate", - Short: "Generate a config file from state management", - Run: func(cmd *cobra.Command, args []string) { - // make sure that we have a token present before trying to make request - if conf.AccessToken == "" { - // check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead - accessToken := os.Getenv("ACCESS_TOKEN") - if accessToken != "" { - conf.AccessToken = accessToken - } else { - // TODO: try and fetch token first if it is needed - if verbose { - log.Warn().Msg("No token found. Attempting to generate conf without one...\n") - } - } - } - - // use cert path from cobra if empty - if conf.CertPath == "" { - conf.CertPath = cacertPath - } - - // show conf as JSON and generators if verbose - if verbose { - b, err := json.MarshalIndent(conf, "", " ") - if err != nil { - log.Error().Err(err).Msg("failed to marshal config") - } - // print the config file as JSON - fmt.Printf("%v\n", string(b)) - } - - // run all of the target recursively until completion if provided - if len(targets) > 0 { - RunTargets(&conf, args, targets...) - } else { - if pluginPath == "" { - log.Error().Msg("no plugin path specified") - return - } - - // load the templates to use - templates := map[string]generator.Template{} - for _, path := range templatePaths { - template := generator.Template{} - template.LoadFromFile(path) - if !template.IsEmpty() { - templates[path] = template - } - } - - params := generator.Params{ - Templates: templates, - } - - // set the client options - // params.ClientOpts = append(params.ClientOpts, client.WithHost(remoteHost)) - if conf.AccessToken != "" { - params.ClientOpts = append(params.ClientOpts, client.WithAccessToken(conf.AccessToken)) - } - if conf.CertPath != "" { - params.ClientOpts = append(params.ClientOpts, client.WithCertPoolFile(conf.CertPath)) - } - - // run generator.Generate() with just plugin path and templates provided - outputBytes, err := generator.Generate(&conf, pluginPath, params) - if err != nil { - log.Error().Err(err).Msg("failed to generate files") - } - - // if we have more than one target and output is set, create configs in directory - outputMap := generator.ConvertContentsToString(outputBytes) - writeOutput(outputBytes, len(targets), len(outputMap)) - } - }, -} - -// Generate files by supplying a list of targets as string values. Currently, -// targets are defined statically in a config file. Targets are ran recursively -// if more targets are nested in a defined target, but will not run additional -// child targets if it is the same as the parent. -// -// NOTE: This may be changed in the future how this is done. -func RunTargets(conf *config.Config, args []string, targets ...string) { - // generate config with each supplied target - for _, target := range targets { - outputBytes, err := generator.GenerateWithTarget(conf, target) - if err != nil { - log.Error().Err(err).Str("target", target).Msg("failed to generate config") - os.Exit(1) - } - - // if we have more than one target and output is set, create configs in directory - outputMap := generator.ConvertContentsToString(outputBytes) - writeOutput(outputBytes, len(targets), len(outputMap)) - - // remove any targets that are the same as current to prevent infinite loop - nextTargets := util.CopyIf(conf.Targets[target].RunTargets, func(nextTarget string) bool { - return nextTarget != target - }) - - // ...then, run any other targets that the current target has - RunTargets(conf, args, nextTargets...) - } -} - -func writeOutput(outputBytes generator.FileMap, targetCount int, templateCount int) { - outputMap := generator.ConvertContentsToString(outputBytes) - if outputPath == "" { - // write only to stdout by default - if len(outputMap) == 1 { - for _, contents := range outputMap { - fmt.Printf("%s\n", string(contents)) - } - } else { - for path, contents := range outputMap { - fmt.Printf("-- file: %s, size: %d B\n%s\n", path, len(contents), string(contents)) - } - } - } else if outputPath != "" && targetCount == 1 && templateCount == 1 { - // write just a single file using provided name - for _, contents := range outputBytes { - err := os.WriteFile(outputPath, contents, 0o644) - if err != nil { - log.Error().Err(err).Str("path", outputPath).Msg("failed to write config file") - os.Exit(1) - } - log.Info().Msgf("wrote file to '%s'\n", outputPath) - } - } else if outputPath != "" && targetCount > 1 && useCompression { - // write multiple files to archive, compress, then save to output path - out, err := os.Create(fmt.Sprintf("%s.tar.gz", outputPath)) - if err != nil { - log.Error().Err(err).Str("path", outputPath).Msg("failed to write archive") - os.Exit(1) - } - files := make([]string, len(outputBytes)) - i := 0 - for path := range outputBytes { - files[i] = path - i++ - } - err = util.CreateArchive(files, out) - if err != nil { - log.Error().Err(err).Str("path", outputPath).Msg("failed to create archive") - os.Exit(1) - } - - } else if outputPath != "" && targetCount > 1 || templateCount > 1 { - // write multiple files in directory using template name - err := os.MkdirAll(filepath.Clean(outputPath), 0o755) - if err != nil { - log.Error().Err(err).Str("path", filepath.Clean(outputPath)).Msg("failed to make output directory") - os.Exit(1) - } - for path, contents := range outputBytes { - filename := filepath.Base(path) - cleanPath := fmt.Sprintf("%s/%s", filepath.Clean(outputPath), filename) - err := os.WriteFile(cleanPath, contents, 0o755) - if err != nil { - log.Error().Err(err).Str("path", path).Msg("failed to write config to file") - os.Exit(1) - } - log.Info().Msgf("wrote file to '%s'\n", cleanPath) - } - } -} - -func init() { - generateCmd.Flags().StringSliceVar(&targets, "target", []string{}, "set the targets to run pre-defined conf") - generateCmd.Flags().StringSliceVar(&templatePaths, "template", []string{}, "set the paths for the Jinja 2 templates to use") - generateCmd.Flags().StringVar(&pluginPath, "plugin", "", "set the generator plugin path") - generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for conf targets") - generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token") - generateCmd.Flags().StringVar(&remoteHost, "host", "http://localhost", "set the remote host") - generateCmd.Flags().BoolVar(&useCompression, "compress", false, "set whether to archive and compress multiple file outputs") - - // requires either 'target' by itself or 'plugin' and 'templates' together - // generateCmd.MarkFlagsOneRequired("target", "plugin") - generateCmd.MarkFlagsMutuallyExclusive("target", "plugin") - generateCmd.MarkFlagsMutuallyExclusive("target", "template") - generateCmd.MarkFlagsRequiredTogether("plugin", "template") - - rootCmd.AddCommand(generateCmd) -} diff --git a/cmd/inspect.go b/cmd/inspect.go deleted file mode 100644 index 735fa7b..0000000 --- a/cmd/inspect.go +++ /dev/null @@ -1,75 +0,0 @@ -package cmd - -import ( - "fmt" - "io/fs" - "path/filepath" - "strings" - - "github.com/OpenCHAMI/configurator/pkg/generator" - "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rodaine/table" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var ( - byTarget bool -) - -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) { - // set up table formatter - table.DefaultHeaderFormatter = func(format string, vals ...interface{}) string { - return strings.ToUpper(fmt.Sprintf(format, vals...)) - } - - // remove duplicate clean paths from CLI - paths := make([]string, len(args)) - for _, path := range args { - paths = append(paths, filepath.Clean(path)) - } - paths = util.RemoveDuplicates(paths) - - // load specific plugins from positional args - var generators = make(map[string]generator.Generator) - for _, path := range paths { - err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - gen, err := generator.LoadPlugin(path) - if err != nil { - return err - } - generators[gen.GetName()] = gen - return nil - }) - - if err != nil { - log.Error().Err(err).Msg("failed to walk directory") - continue - } - } - - // print all generator plugin information found - tbl := table.New("Name", "Version", "Description") - for _, g := range generators { - tbl.AddRow(g.GetName(), g.GetVersion(), g.GetDescription()) - } - if len(generators) > 0 { - tbl.Print() - } - }, -} - -func init() { - inspectCmd.Flags().BoolVar(&byTarget, "by-target", false, "set whether to ") - rootCmd.AddCommand(inspectCmd) -} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..e69de29 diff --git a/cmd/profiles.go b/cmd/profiles.go new file mode 100644 index 0000000..e69de29 diff --git a/cmd/render.go b/cmd/render.go new file mode 100644 index 0000000..e69de29 diff --git a/cmd/root.go b/cmd/root.go index 583ee6c..2e70c6a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,35 +4,20 @@ import ( "fmt" "os" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -var ( - conf config.Config - configPath string - cacertPath string - verbose bool - targets []string - outputPath string - accessToken string - remoteHost string -) - -var rootCmd = &cobra.Command{ - Use: "configurator", - Short: "Dynamically generate files defined by generators", +var rootCmd = cobra.Command{ + Use: "configurator", Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - cmd.Help() - os.Exit(0) - } + }, } func Execute() { + // run initialization code first + initEnv() + if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -40,39 +25,13 @@ func Execute() { } func init() { - cobra.OnInitialize(InitConfig) - rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config path") - rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set to enable verbose output") - rootCmd.PersistentFlags().StringVar(&cacertPath, "cacert", "", "path to CA cert. (defaults to system CAs)") + // initialize the config a single time } -func InitConfig() { - // empty from not being set - if configPath != "" { - exists, err := util.PathExists(configPath) - if err != nil { - log.Error().Err(err).Str("path", configPath).Msg("failed to load config") - os.Exit(1) - } else if exists { - conf = config.Load(configPath) - } else { - // show error and exit since a path was specified - log.Error().Str("path", configPath).Msg("config file not found") - os.Exit(1) - } - } else { - // set to the default value and create a new one - configPath = "./config.yaml" - conf = config.New() - } +func initConfigFromFile(path string) { + +} + +func initEnv() { - // - // set environment variables to override config values - // - - // set the JWKS url if we find the CONFIGURATOR_JWKS_URL environment variable - jwksUrl := os.Getenv("CONFIGURATOR_JWKS_URL") - if jwksUrl != "" { - conf.Server.Jwks.Uri = jwksUrl - } } diff --git a/cmd/serve.go b/cmd/serve.go deleted file mode 100644 index 48e3629..0000000 --- a/cmd/serve.go +++ /dev/null @@ -1,67 +0,0 @@ -//go:build server || all -// +build server all - -package cmd - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - - "github.com/OpenCHAMI/configurator/pkg/server" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var serveCmd = &cobra.Command{ - Use: "serve", - Short: "Start configurator as a server and listen for requests", - Run: func(cmd *cobra.Command, args []string) { - // make sure that we have a token present before trying to make request - if conf.AccessToken == "" { - // check if ACCESS_TOKEN env var is set if no access token is provided and use that instead - accessToken := os.Getenv("ACCESS_TOKEN") - if accessToken != "" { - conf.AccessToken = accessToken - } else { - if verbose { - log.Warn().Msg("No token found. Continuing without one...\n") - } - } - } - - // show config as JSON and generators if verbose - if verbose { - b, err := json.MarshalIndent(conf, "", "\t") - if err != nil { - log.Error().Err(err).Msg("failed to marshal config") - os.Exit(1) - } - fmt.Printf("%v\n", string(b)) - } - - // start listening with the server - var ( - s *server.Server = server.New(&conf) - err error = s.Serve() - ) - if errors.Is(err, http.ErrServerClosed) { - if verbose { - log.Info().Msg("server closed") - } - } else if err != nil { - log.Error().Err(err).Msg("failed to start server") - os.Exit(1) - } - }, -} - -func init() { - serveCmd.Flags().StringVar(&conf.Server.Host, "host", conf.Server.Host, "set the server host and port") - // serveCmd.Flags().StringVar(&pluginPath, "plugin", "", "set the generator plugins directory path") - serveCmd.Flags().StringVar(&conf.Server.Jwks.Uri, "jwks-uri", conf.Server.Jwks.Uri, "set the JWKS url to fetch public key") - serveCmd.Flags().IntVar(&conf.Server.Jwks.Retries, "jwks-fetch-retries", conf.Server.Jwks.Retries, "set the JWKS fetch retry count") - rootCmd.AddCommand(serveCmd) -} diff --git a/cmd/upload.go b/cmd/upload.go new file mode 100644 index 0000000..9fc1f12 --- /dev/null +++ b/cmd/upload.go @@ -0,0 +1,14 @@ +package cmd + +import "github.com/spf13/cobra" + +var uploadCmd = &cobra.Command{ + Use: "upload", + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +func init() { + rootCmd.AddCommand(uploadCmd) +} diff --git a/examples/plugin/test.go b/examples/plugin/test.go index 58aa310..6289e1c 100644 --- a/examples/plugin/test.go +++ b/examples/plugin/test.go @@ -1,8 +1,8 @@ package main import ( - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/generator" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/generator" ) type TestGenerator struct{} diff --git a/examples/templates/conman.jinja b/examples/templates/conman.jinja index 249994c..69e779c 100644 --- a/examples/templates/conman.jinja +++ b/examples/templates/conman.jinja @@ -5,7 +5,7 @@ # Description: {{ plugin_description }} # # Source code: https://github.com/OpenCHAMI/configurator -# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins +# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins # SERVER keepalive=ON SERVER logdir="/var/log/conman" diff --git a/examples/templates/dhcpd.jinja b/examples/templates/dhcpd.jinja index 5613f73..264e590 100644 --- a/examples/templates/dhcpd.jinja +++ b/examples/templates/dhcpd.jinja @@ -5,7 +5,7 @@ # Description: {{ plugin_description }} # # Source code: https://github.com/OpenCHAMI/configurator -# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins +# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins # allow booting; allow bootp; diff --git a/examples/templates/dnsmasq.jinja b/examples/templates/dnsmasq.jinja index d187a9a..1cc4dab 100644 --- a/examples/templates/dnsmasq.jinja +++ b/examples/templates/dnsmasq.jinja @@ -5,6 +5,6 @@ # Description: {{ plugin_description }} # # Source code: https://github.com/OpenCHAMI/configurator -# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins +# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins # {{ dhcp_hosts }} diff --git a/examples/templates/powerman.jinja b/examples/templates/powerman.jinja index 5808124..44c13e4 100644 --- a/examples/templates/powerman.jinja +++ b/examples/templates/powerman.jinja @@ -5,7 +5,7 @@ # Description: {{ plugin_description }} # # Source code: https://github.com/OpenCHAMI/configurator -# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins +# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins # include "/etc/powerman/ipmipower.dev" include "/etc/powerman/ipmi.dev" diff --git a/examples/templates/test.j2 b/examples/templates/test.j2 index df2bb4e..17bd6b6 100644 --- a/examples/templates/test.j2 +++ b/examples/templates/test.j2 @@ -5,7 +5,7 @@ # Description: {{ plugin_description }} # # Source code: https://github.com/OpenCHAMI/configurator -# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins +# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins # # TODO: test variables diff --git a/go.mod b/go.mod index e9ff5f2..dcb6e87 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/OpenCHAMI/configurator +module git.towk2.me/towk/configurator go 1.21.5 @@ -11,7 +11,6 @@ require ( github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700 github.com/rodaine/table v1.2.0 github.com/rs/zerolog v1.33.0 - github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 gopkg.in/yaml.v2 v2.4.0 @@ -35,6 +34,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/segmentio/asm v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/sys v0.22.0 // indirect diff --git a/main.go b/main.go index a3e5a86..0828d29 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,6 @@ package main -import "github.com/OpenCHAMI/configurator/cmd" +import "git.towk2.me/towk/configurator/cmd" func main() { cmd.Execute() diff --git a/pkg/auth.go b/pkg/auth/auth.go similarity index 100% rename from pkg/auth.go rename to pkg/auth/auth.go diff --git a/pkg/client/client.go b/pkg/client/client.go deleted file mode 100644 index a4b7454..0000000 --- a/pkg/client/client.go +++ /dev/null @@ -1,66 +0,0 @@ -package client - -import ( - "crypto/tls" - "crypto/x509" - "net" - "net/http" - "os" - "time" -) - -type Option func(*Params) -type Params struct { - Host string `yaml:"host"` - AccessToken string `yaml:"access-token"` - Transport *http.Transport -} - -func ToParams(opts ...Option) *Params { - params := &Params{} - for _, opt := range opts { - opt(params) - } - return params -} - -func WithHost(host string) Option { - return func(c *Params) { - c.Host = host - } -} - -func WithAccessToken(token string) Option { - return func(c *Params) { - c.AccessToken = token - } -} - -func WithCertPool(certPool *x509.CertPool) Option { - return func(c *Params) { - c.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: certPool, - InsecureSkipVerify: true, - }, - DisableKeepAlives: true, - Dial: (&net.Dialer{ - Timeout: 120 * time.Second, - KeepAlive: 120 * time.Second, - }).Dial, - TLSHandshakeTimeout: 120 * time.Second, - ResponseHeaderTimeout: 120 * time.Second, - } - } -} - -// FIXME: Need to check for errors when reading from a file -func WithCertPoolFile(certPath string) Option { - if certPath == "" { - return func(sc *Params) {} - } - cacert, _ := os.ReadFile(certPath) - certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(cacert) - return WithCertPool(certPool) -} diff --git a/pkg/client/smd.go b/pkg/client/smd.go deleted file mode 100644 index eb80ce8..0000000 --- a/pkg/client/smd.go +++ /dev/null @@ -1,174 +0,0 @@ -package client - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/rs/zerolog/log" -) - -// An struct that's meant to extend functionality of the base HTTP client by -// adding commonly made requests to SMD. The implemented functions are can be -// used in generator plugins to fetch data when it is needed to substitute -// values for the Jinja templates used. -type SmdClient struct { - http.Client `json:"-" yaml:"-"` - Host string `yaml:"host"` - Port int `yaml:"port"` - AccessToken string `yaml:"access-token"` -} - -// Constructor function that allows supplying Option arguments to set -// things like the host, port, access token, etc. -func NewSmdClient(opts ...Option) SmdClient { - var ( - params = ToParams(opts...) - client = SmdClient{ - Host: params.Host, - AccessToken: params.AccessToken, - } - ) - - return client -} - -// Fetch the ethernet interfaces from SMD service using its API. An access token may be required if the SMD -// service SMD_JWKS_URL envirnoment variable is set. -func (client *SmdClient) FetchEthernetInterfaces(verbose bool) ([]configurator.EthernetInterface, error) { - var ( - eths = []configurator.EthernetInterface{} - bytes []byte - err error - ) - // make request to SMD endpoint - bytes, err = client.makeRequest("/Inventory/EthernetInterfaces") - if err != nil { - return nil, fmt.Errorf("failed to read HTTP response: %v", err) - } - - // unmarshal response body JSON and extract in object - err = json.Unmarshal(bytes, ðs) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %v", err) - } - - // print what we got if verbose is set - if verbose { - log.Info().Str("ethernet_interfaces", string(bytes)).Msg("found interfaces") - } - - return eths, nil -} - -// Fetch the components from SMD using its API. An access token may be required if the SMD -// service SMD_JWKS_URL envirnoment variable is set. -func (client *SmdClient) FetchComponents(verbose bool) ([]configurator.Component, error) { - var ( - comps = []configurator.Component{} - bytes []byte - err error - ) - // make request to SMD endpoint - bytes, err = client.makeRequest("/State/Components") - if err != nil { - return nil, fmt.Errorf("failed to make HTTP request: %v", err) - } - - // make sure our response is actually JSON - if !json.Valid(bytes) { - return nil, fmt.Errorf("expected valid JSON response: %v", string(bytes)) - } - - // unmarshal response body JSON and extract in object - var tmp map[string]any - err = json.Unmarshal(bytes, &tmp) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %v", err) - } - bytes, err = json.Marshal(tmp["RedfishEndpoints"].([]any)) - if err != nil { - return nil, fmt.Errorf("failed to marshal JSON: %v", err) - } - err = json.Unmarshal(bytes, &comps) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %v", err) - } - - // print what we got if verbose is set - if verbose { - log.Info().Str("components", string(bytes)).Msg("found components") - } - - return comps, nil -} - -// TODO: improve implementation of this function -func (client *SmdClient) FetchRedfishEndpoints(verbose bool) ([]configurator.RedfishEndpoint, error) { - var ( - eps = []configurator.RedfishEndpoint{} - tmp map[string]any - ) - - // make initial request to get JSON with 'RedfishEndpoints' as property - b, err := client.makeRequest("/Inventory/RedfishEndpoints") - if err != nil { - return nil, fmt.Errorf("failed to make HTTP resquest: %v", err) - } - // make sure response is in JSON - if !json.Valid(b) { - return nil, fmt.Errorf("expected valid JSON response: %v", string(b)) - } - err = json.Unmarshal(b, &tmp) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %v", err) - } - - // marshal RedfishEndpoint JSON back to configurator.RedfishEndpoint - b, err = json.Marshal(tmp["RedfishEndpoints"].([]any)) - if err != nil { - return nil, fmt.Errorf("failed to marshal JSON: %v", err) - } - err = json.Unmarshal(b, &eps) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %v", err) - } - - // show the final result - if verbose { - log.Info().Str("redfish_endpoints", string(b)).Msg("found redfish endpoints") - } - - return eps, nil -} - -func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) { - if client == nil { - return nil, fmt.Errorf("client is nil") - } - - // fetch DHCP related information from SMD's endpoint: - url := fmt.Sprintf("%s/hsm/v2%s", client.Host, endpoint) - req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{})) - if err != nil { - return nil, fmt.Errorf("failed to create new HTTP request: %v", err) - } - - // include access token in authorzation header if found - // NOTE: This shouldn't be needed for this endpoint since it's public - if client.AccessToken != "" { - req.Header.Add("Authorization", "Bearer "+client.AccessToken) - } - - // make the request to SMD - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make request: %v", err) - } - - // read the contents of the response body - return io.ReadAll(res.Body) -} diff --git a/pkg/config/config.go b/pkg/config/config.go deleted file mode 100644 index c20797b..0000000 --- a/pkg/config/config.go +++ /dev/null @@ -1,100 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - - "github.com/rs/zerolog/log" - - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "gopkg.in/yaml.v2" -) - -type Jwks struct { - Uri string `yaml:"uri"` - Retries int `yaml:"retries,omitempty"` -} - -type Server struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - Jwks Jwks `yaml:"jwks,omitempty"` -} - -type Config struct { - Version string `yaml:"version,omitempty"` - Server Server `yaml:"server,omitempty"` - SmdClient client.SmdClient `yaml:"smd,omitempty"` - AccessToken string `yaml:"access-token,omitempty"` - Targets map[string]configurator.Target `yaml:"targets,omitempty"` - PluginDirs []string `yaml:"plugins,omitempty"` - CertPath string `yaml:"cacert,omitempty"` -} - -// Creates a new config with default parameters. -func New() Config { - return Config{ - Version: "", - SmdClient: client.SmdClient{Host: "http://127.0.0.1:27779"}, - Targets: map[string]configurator.Target{}, - PluginDirs: []string{}, - Server: Server{ - Host: "127.0.0.1:3334", - Jwks: Jwks{ - Uri: "", - Retries: 5, - }, - }, - } -} - -func Load(path string) Config { - var c Config = New() - file, err := os.ReadFile(path) - if err != nil { - log.Error().Err(err).Msg("failed to read config file") - return c - } - err = yaml.Unmarshal(file, &c) - if err != nil { - log.Error().Err(err).Msg("failed to unmarshal config") - return c - } - return c -} - -func (config *Config) Save(path string) { - path = filepath.Clean(path) - if path == "" || path == "." { - path = "config.yaml" - } - data, err := yaml.Marshal(config) - if err != nil { - log.Error().Err(err).Msg("failed to marshal config") - return - } - err = os.WriteFile(path, data, 0o644) - if err != nil { - log.Error().Err(err).Msg("failed to write default config file") - return - } -} - -func SaveDefault(path string) { - path = filepath.Clean(path) - if path == "" || path == "." { - path = "config.yaml" - } - var c = New() - data, err := yaml.Marshal(c) - if err != nil { - log.Error().Err(err).Msg("failed to marshal config") - return - } - err = os.WriteFile(path, data, 0o644) - if err != nil { - log.Error().Err(err).Msg("failed to write default config file") - return - } -} diff --git a/pkg/configurator.go b/pkg/configurator.go deleted file mode 100644 index 7a19a36..0000000 --- a/pkg/configurator.go +++ /dev/null @@ -1,66 +0,0 @@ -package configurator - -import "encoding/json" - -type Target struct { - Plugin string `yaml:"plugin,omitempty"` // Set the plugin or it's path - TemplatePaths []string `yaml:"templates,omitempty"` // Set the template paths - FilePaths []string `yaml:"files,omitempty"` // Set the file paths - RunTargets []string `yaml:"targets,omitempty"` // Set additional targets to run -} - -type IPAddr struct { - IpAddress string `json:"IPAddress"` - Network string `json:"Network"` -} - -type EthernetInterface struct { - Id string - Description string - MacAddress string - LastUpdate string - ComponentId string - Type string - IpAddresses []IPAddr -} - -type Component struct { - ID string `json:"ID"` - Type string `json:"Type"` - State string `json:"State,omitempty"` - Flag string `json:"Flag,omitempty"` - Enabled *bool `json:"Enabled,omitempty"` - SwStatus string `json:"SoftwareStatus,omitempty"` - Role string `json:"Role,omitempty"` - SubRole string `json:"SubRole,omitempty"` - NID json.Number `json:"NID,omitempty"` - Subtype string `json:"Subtype,omitempty"` - NetType string `json:"NetType,omitempty"` - Arch string `json:"Arch,omitempty"` - Class string `json:"Class,omitempty"` - ReservationDisabled bool `json:"ReservationDisabled,omitempty"` - Locked bool `json:"Locked,omitempty"` -} - -type RedfishEndpoint struct { - ID string `json:"ID"` - Type string `json:"Type"` - Name string `json:"Name,omitempty"` // user supplied descriptive name - Hostname string `json:"Hostname"` - Domain string `json:"Domain"` - FQDN string `json:"FQDN"` - Enabled bool `json:"Enabled"` - UUID string `json:"UUID,omitempty"` - User string `json:"User"` - Password string `json:"Password"` // Temporary until more secure method - UseSSDP bool `json:"UseSSDP,omitempty"` - MACRequired bool `json:"MACRequired,omitempty"` - MACAddr string `json:"MACAddr,omitempty"` - IPAddr string `json:"IPAddress,omitempty"` -} - -type Node struct { -} - -type BMC struct { -} diff --git a/pkg/generator/conman.go b/pkg/generator/conman.go index ccadaa6..be8e882 100644 --- a/pkg/generator/conman.go +++ b/pkg/generator/conman.go @@ -3,10 +3,10 @@ package generator import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + configurator "git.towk2.me/towk/configurator/pkg" + "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type Conman struct{} diff --git a/pkg/generator/coredhcp.go b/pkg/generator/coredhcp.go index 0a256e9..ccc79cd 100644 --- a/pkg/generator/coredhcp.go +++ b/pkg/generator/coredhcp.go @@ -3,8 +3,8 @@ package generator import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type CoreDhcp struct{} diff --git a/pkg/generator/dhcpd.go b/pkg/generator/dhcpd.go index cc32a48..75a41bb 100644 --- a/pkg/generator/dhcpd.go +++ b/pkg/generator/dhcpd.go @@ -3,10 +3,10 @@ package generator import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + configurator "git.towk2.me/towk/configurator/pkg" + "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type DHCPd struct{} diff --git a/pkg/generator/dnsmasq.go b/pkg/generator/dnsmasq.go index 8db68ba..16ea6c9 100644 --- a/pkg/generator/dnsmasq.go +++ b/pkg/generator/dnsmasq.go @@ -3,10 +3,10 @@ package generator import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + configurator "git.towk2.me/towk/configurator/pkg" + "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type DNSMasq struct{} diff --git a/pkg/generator/example.go b/pkg/generator/example.go index f18abe4..12091ba 100644 --- a/pkg/generator/example.go +++ b/pkg/generator/example.go @@ -3,8 +3,8 @@ package generator import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type Example struct { diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 3397e36..3667130 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -7,10 +7,10 @@ import ( "path/filepath" "plugin" - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + configurator "git.towk2.me/towk/configurator/pkg" + "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" "github.com/rs/zerolog/log" ) diff --git a/pkg/generator/hostfile.go b/pkg/generator/hostfile.go index 7ce26c8..a9f92e5 100644 --- a/pkg/generator/hostfile.go +++ b/pkg/generator/hostfile.go @@ -3,8 +3,8 @@ package generator import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type Hostfile struct{} diff --git a/pkg/generator/params.go b/pkg/generator/params.go index e54420a..e9037ec 100644 --- a/pkg/generator/params.go +++ b/pkg/generator/params.go @@ -1,9 +1,9 @@ package generator import ( - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" + configurator "git.towk2.me/towk/configurator/pkg" + "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/configurator/pkg/config" ) type ( diff --git a/pkg/generator/powerman.go b/pkg/generator/powerman.go index 08745e5..d8e376f 100644 --- a/pkg/generator/powerman.go +++ b/pkg/generator/powerman.go @@ -3,8 +3,8 @@ package generator import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type Powerman struct{} diff --git a/pkg/generator/syslog.go b/pkg/generator/syslog.go index 67b28cf..450dcbc 100644 --- a/pkg/generator/syslog.go +++ b/pkg/generator/syslog.go @@ -3,8 +3,8 @@ package generator import ( "fmt" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" ) type Syslog struct{} diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go index 321076c..3028949 100644 --- a/pkg/generator/templates.go +++ b/pkg/generator/templates.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/OpenCHAMI/configurator/pkg/util" + "git.towk2.me/towk/configurator/pkg/util" "github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2/exec" "github.com/rs/zerolog/log" diff --git a/pkg/generator/warewulf.go b/pkg/generator/warewulf.go index bdfbda1..5a7c803 100644 --- a/pkg/generator/warewulf.go +++ b/pkg/generator/warewulf.go @@ -5,9 +5,9 @@ import ( "maps" "strings" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/util" + "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/configurator/pkg/config" + "git.towk2.me/towk/configurator/pkg/util" "github.com/rs/zerolog/log" ) diff --git a/pkg/plugin.go b/pkg/plugin.go new file mode 100644 index 0000000..602cfce --- /dev/null +++ b/pkg/plugin.go @@ -0,0 +1,14 @@ +package configurator + +type Plugin interface { + // plugin data + Name() string + Version() string + Description() string + Metadata() map[string]string + + // run the plugin + Init() error + Run() error + Cleanup() error +} diff --git a/pkg/server/server.go b/pkg/server/server.go deleted file mode 100644 index d10ccd7..0000000 --- a/pkg/server/server.go +++ /dev/null @@ -1,327 +0,0 @@ -//go:build server || all -// +build server all - -package server - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "time" - - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/client" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/generator" - "github.com/OpenCHAMI/jwtauth/v5" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/rs/zerolog" - - openchami_authenticator "github.com/openchami/chi-middleware/auth" - openchami_logger "github.com/openchami/chi-middleware/log" - "github.com/rs/zerolog/log" -) - -var ( - tokenAuth *jwtauth.JWTAuth = nil -) - -type Jwks struct { - Uri string - Retries int -} -type Server struct { - *http.Server - Config *config.Config - Jwks Jwks `yaml:"jwks"` - GeneratorParams generator.Params - TokenAuth *jwtauth.JWTAuth - Targets map[string]Target -} - -type Target struct { - Name string `json:"name"` - PluginPath string `json:"plugin"` - Templates []generator.Template `json:"templates"` -} - -// Constructor to make a new server instance with an optional config. -func New(conf *config.Config) *Server { - // create default config if none supplied - if conf == nil { - c := config.New() - conf = &c - } - newServer := &Server{ - Config: conf, - Server: &http.Server{Addr: conf.Server.Host}, - Jwks: Jwks{ - Uri: conf.Server.Jwks.Uri, - Retries: conf.Server.Jwks.Retries, - }, - } - // load templates for server from config - newServer.loadTargets() - log.Debug().Any("targets", newServer.Targets).Msg("new server targets") - return newServer -} - -// Main function to start up configurator as a service. -func (s *Server) Serve() error { - // Setup logger - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - - // set the server address with config values - s.Server.Addr = s.Config.Server.Host - - // fetch JWKS public key from authorization server - if s.Config.Server.Jwks.Uri != "" && tokenAuth == nil { - for i := 0; i < s.Config.Server.Jwks.Retries; i++ { - var err error - tokenAuth, err = configurator.FetchPublicKeyFromURL(s.Config.Server.Jwks.Uri) - if err != nil { - log.Error().Err(err).Msgf("failed to fetch JWKS") - continue - } - break - } - } - - // create client with opts to use to fetch data from SMD - opts := []client.Option{ - client.WithHost(s.Config.SmdClient.Host), - client.WithAccessToken(s.Config.AccessToken), - client.WithCertPoolFile(s.Config.CertPath), - } - - // create new go-chi router with its routes - router := chi.NewRouter() - router.Use(middleware.RequestID) - router.Use(middleware.RealIP) - router.Use(middleware.Logger) - router.Use(middleware.Recoverer) - router.Use(middleware.StripSlashes) - router.Use(middleware.Timeout(60 * time.Second)) - router.Use(openchami_logger.OpenCHAMILogger(logger)) - if s.Config.Server.Jwks.Uri != "" { - router.Group(func(r chi.Router) { - r.Use( - jwtauth.Verifier(tokenAuth), - openchami_authenticator.AuthenticatorWithRequiredClaims(tokenAuth, []string{"sub", "iss", "aud"}), - ) - - // protected routes if using auth - r.HandleFunc("/generate", s.Generate(opts...)) - r.Post("/targets", s.createTarget) - }) - } else { - // public routes without auth - router.HandleFunc("/generate", s.Generate(opts...)) - router.Post("/targets", s.createTarget) - } - - // always available public routes go here (none at the moment) - router.HandleFunc("/configurator/status", s.GetStatus) - - s.Handler = router - return s.ListenAndServe() -} - -// TODO: implement a way to shut the server down -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. -func (s *Server) Generate(opts ...client.Option) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // get all of the expect query URL params and validate - var ( - targetParam string = r.URL.Query().Get("target") - target *Target = s.getTarget(targetParam) - outputs generator.FileMap - err error - ) - s.GeneratorParams = parseGeneratorParams(r, target, opts...) - if targetParam == "" { - err = writeErrorResponse(w, "must specify a target") - log.Error().Err(err).Msg("failed to parse generator params") - return - } - - // try to generate with target supplied by client first - if target != nil { - log.Debug().Any("target", target).Msg("target for Generate()") - outputs, err = generator.Generate(s.Config, target.PluginPath, s.GeneratorParams) - log.Debug().Any("outputs map", outputs).Msgf("after generate") - if err != nil { - log.Error().Err(err).Msg("failed to generate file") - return - } - } else { - // try and generate a new config file from supplied params - log.Debug().Str("target", targetParam).Msg("target for GenerateWithTarget()") - outputs, err = generator.GenerateWithTarget(s.Config, targetParam) - if err != nil { - writeErrorResponse(w, "failed to generate file") - log.Error().Err(err).Msgf("failed to generate file with target '%s'", target) - return - } - } - - // marshal output to JSON then send response to client - tmp := generator.ConvertContentsToString(outputs) - b, err := json.Marshal(tmp) - if err != nil { - writeErrorResponse(w, "failed to marshal output: %v", err) - log.Error().Err(err).Msg("failed to marshal output") - return - } - _, err = w.Write(b) - if err != nil { - writeErrorResponse(w, "failed to write response: %v", err) - log.Error().Err(err).Msg("failed to write response") - return - } - } -} - -func (s *Server) loadTargets() { - // make sure the map is initialized first - if s.Targets == nil { - s.Targets = make(map[string]Target) - } - // add default generator targets - for name, _ := range generator.DefaultGenerators { - serverTarget := Target{ - Name: name, - PluginPath: name, - } - s.Targets[name] = serverTarget - } - // add targets from config to server (overwrites default targets) - for name, target := range s.Config.Targets { - serverTarget := Target{ - Name: name, - } - // only overwrite plugin path if it's set - if target.Plugin != "" { - serverTarget.PluginPath = target.Plugin - } else { - serverTarget.PluginPath = name - } - // add templates using template paths from config - for _, templatePath := range target.TemplatePaths { - template := generator.Template{} - template.LoadFromFile(templatePath) - serverTarget.Templates = append(serverTarget.Templates, template) - } - s.Targets[name] = serverTarget - } -} - -func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - data := map[string]any{ - "code": 200, - "message": "Configurator is healthy", - } - err := json.NewEncoder(w).Encode(data) - if err != nil { - fmt.Printf("failed to encode JSON: %v\n", err) - return - } -} - -// Create a new target with name, generator, templates, and files. -// -// Example: -// -// curl -X POST /target?name=test&plugin=dnsmasq -// -// TODO: need to implement template managing API first in "internal/generator/templates" or something -func (s *Server) createTarget(w http.ResponseWriter, r *http.Request) { - var ( - target = Target{} - bytes []byte - err error - ) - if r == nil { - err = writeErrorResponse(w, "request is invalid") - log.Error().Err(err).Msg("request == nil") - return - } - - bytes, err = io.ReadAll(r.Body) - if err != nil { - writeErrorResponse(w, "failed to read response body: %v", err) - log.Error().Err(err).Msg("failed to read response body") - return - } - defer r.Body.Close() - - err = json.Unmarshal(bytes, &target) - if err != nil { - writeErrorResponse(w, "failed to unmarshal target: %v", err) - log.Error().Err(err).Msg("failed to unmarshal target") - return - } - - // make sure a plugin and at least one template is supplied - if target.Name == "" { - err = writeErrorResponse(w, "target name is required") - log.Error().Err(err).Msg("set target as a URL query parameter") - return - } - if target.PluginPath == "" { - err = writeErrorResponse(w, "generator name is required") - log.Error().Err(err).Msg("must supply a generator name") - return - } - if len(target.Templates) <= 0 { - writeErrorResponse(w, "requires at least one template") - log.Error().Err(err).Msg("must supply at least one template") - return - } - - s.Targets[target.Name] = target - -} - -func (s *Server) getTarget(name string) *Target { - t, ok := s.Targets[name] - if ok { - return &t - } - return nil -} - -// Wrapper function to simplify writting error message responses. This function -// is only intended to be used with the service and nothing else. -func writeErrorResponse(w http.ResponseWriter, format string, a ...any) error { - errmsg := fmt.Sprintf(format, a...) - bytes, _ := json.Marshal(map[string]any{ - "level": "error", - "time": time.Now().Unix(), - "message": errmsg, - }) - http.Error(w, string(bytes), http.StatusInternalServerError) - return fmt.Errorf(errmsg) -} - -func parseGeneratorParams(r *http.Request, target *Target, opts ...client.Option) generator.Params { - var params = generator.Params{ - ClientOpts: opts, - Templates: make(map[string]generator.Template, len(target.Templates)), - } - for i, template := range target.Templates { - params.Templates[fmt.Sprintf("%s_%d", target.Name, i)] = template - } - return params -} diff --git a/pkg/service/profile.go b/pkg/service/profile.go new file mode 100644 index 0000000..cbfe5c0 --- /dev/null +++ b/pkg/service/profile.go @@ -0,0 +1,184 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + + "github.com/go-chi/chi/v5" +) + +type Profile struct { + ID string `json:"id"` // profile ID + Description string `json:"description"` // profile description + Tags []string `json:"tags"` // tags used for ... + Paths []string `json:"paths"` // paths to download + Plugins []string `json:"plugins"` // plugins to run + Data map[string]any `json:"data"` // include render data +} + +func (s *Service) GetProfiles() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + path = s.RootPath + PROFILES_RELPATH + profiles []*Profile + contents []byte + err error + ) + + // walk profiles directory to load all profiles + err = filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + // skip directories + if info.IsDir() { + return nil + } + + // read file contents + var profile *Profile + profile, err = LoadProfile(path) + if err != nil { + return err + } + + profiles = append(profiles, profile) + + fmt.Println(path, info.Size()) + return nil + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + // marshal and send all the profiles + contents, err = json.Marshal(profiles) + if err != nil { + http.Error(w, fmt.Sprintf("failed to marshal profiles: %v", err), http.StatusInternalServerError) + } + + _, err = w.Write(contents) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} + +// func (s *Service) CreateProfiles() http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// var ( +// path = chi.URLParam(r, "path") +// err error +// ) + +// } +// } + +func (s *Service) GetProfile() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + id = chi.URLParam(r, "id") + path = s.BuildProfilePath(id) + profile *Profile + contents []byte + err error + ) + + profile, err = LoadProfile(path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + contents, err = json.Marshal(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, err = w.Write(contents) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func (s *Service) CreateProfile() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + type input struct { + path string `json:"path"` + profile *Profile `json:"profile"` + } + var ( + body []byte + in input + err error + ) + + body, err = io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // use the request info to build profile + err = json.Unmarshal(body, &in) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // create a new profile on disk + os.WriteFile() + + w.WriteHeader(http.StatusOK) + } +} + +func (s *Service) CreateProfileVar() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) DeleteProfileVar() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) GetProfileData() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) GetProfileVar() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) CreateProfilePath() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) DeleteProfilePath() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) GetProfilePath() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) GetPlugins() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) CreatePlugins() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func (s *Service) DeletePlugins() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} +} diff --git a/pkg/service/routes.go b/pkg/service/routes.go new file mode 100644 index 0000000..f0e55ab --- /dev/null +++ b/pkg/service/routes.go @@ -0,0 +1,38 @@ +package service + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func (s *Service) Download() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func (s *Service) Upload() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func (s *Service) List() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func (s *Service) GetStatus(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + data := map[string]any{ + "code": 200, + "message": "Configurator is healthy", + } + err := json.NewEncoder(w).Encode(data) + if err != nil { + fmt.Printf("failed to encode JSON: %v\n", err) + return + } +} diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..6c3467d --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,122 @@ +package service + +import ( + "encoding/json" + "fmt" + "os" + "time" + + configurator "git.towk2.me/towk/configurator/pkg" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" +) + +const ( + PLUGINS_RELPATH = "/plugins" + TEMPLATES_RELPATH = "/templates" + PROFILES_RELPATH = "/profiles" +) + +type Service struct { + RootPath string `yaml:"root,omitempty"` + Environment map[string]string + + // max counts + PluginsMaxCount int + ProfilesMaxCount int +} + +// New creates the directories at specified path +func New() *Service { + return &Service{ + RootPath: ".", + Environment: map[string]string{ + "CONFIGURATOR_HOST_URI": "", + "ACCESS_TOKEN": "", + }, + PluginsMaxCount: 64, + ProfilesMaxCount: 256, + } +} + +// Serve() starts the configurator service and waits for requests. +func (s *Service) Serve() error { + router := chi.NewRouter() + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(middleware.Logger) + router.Use(middleware.Recoverer) + router.Use(middleware.StripSlashes) + router.Use(middleware.Timeout(60 * time.Second)) + + if s.requireAuth() { + + } else { + // general + router.Get("/download", s.Download()) + router.Post("/upload", s.Upload()) + router.Get("/list", s.List()) + + // profiles + router.Get("/profiles", s.GetProfiles()) + // router.Post("/profiles", s.CreateProfiles()) + router.Get("/profile/{id}", s.GetProfile()) + router.Post("/profile/{id}", s.CreateProfile()) + router.Post("/profile/{id}/data/{varname}", s.CreateProfileVar()) + router.Delete("/profile/{id}/data/{varname}", s.DeleteProfileVar()) + router.Get("/profile/{id}/data", s.GetProfileData()) + router.Get("/profile/{id}/data/{varname}", s.GetProfileVar()) + router.Post("/profile/{id}/paths/{path}", s.CreateProfilePath()) + router.Delete("/profile/{id}/paths/{path}", s.DeleteProfilePath()) + router.Get("/profile/{id}/paths/{path}", s.GetProfilePath()) + + // plugins + router.Get("/plugins", s.GetPlugins()) + router.Post("/plugins", s.CreatePlugins()) + router.Delete("/plugins/{id}", s.DeletePlugins()) + } + + // always available public routes go here + router.HandleFunc("/status", s.GetStatus) + return nil +} + +func (s *Service) requireAuth() bool { + return false +} + +func (s *Service) FetchJwks(uri string) { + +} + +func LoadProfile(path string) (*Profile, error) { + return LoadFromJSONFile[Profile](path) +} + +func LoadPlugin(path string) (*configurator.Plugin, error) { + return LoadFromJSONFile[configurator.Plugin](path) +} + +func LoadFromJSONFile[T any](path string) (*T, error) { + var ( + res *T + contents []byte + err error + ) + + contents, err = os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read plugin file: %v", err) + } + + err = json.Unmarshal(contents, &res) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal plugin: %v", err) + } + + return res, err +} + +func (s *Service) BuildProfilePath(id string) string { + return s.RootPath + PLUGINS_RELPATH + "/" + id +} diff --git a/res/archlinux/PKGBUILD b/res/archlinux/PKGBUILD index 56159b3..5796121 100644 --- a/res/archlinux/PKGBUILD +++ b/res/archlinux/PKGBUILD @@ -9,7 +9,7 @@ license=('MIT') groups=("openchami") provides=('configurator') conflicts=('configurator') -https://github.com/OpenCHAMI/configurator/releases/download/v0.1.0-alpha/configurator +https://git.towk2.me/towk/configurator/releases/download/v0.1.0-alpha/configurator source_x86_64=( "${url}/releases/download/v0.1.0-alpha/${pkgname}.tar.gz" ) diff --git a/tests/generate_local.hurl b/tests/generate_local.hurl deleted file mode 100644 index d198e46..0000000 --- a/tests/generate_local.hurl +++ /dev/null @@ -1,22 +0,0 @@ -## -## Run these tests after starting server with `configurator serve...` -## - -# Generate a `example` config with default plugin and template -GET http://127.0.0.1:3334/generate?target=example -HTTP 200 - -# Create a new target using the API -POST http://127.0.0.1:3334/targets -{ - "name": "test", - "plugin": "example", - "templates": [{ - "contents": "This is an example template used with the example plugin." - }] -} -HTTP 200 - -# Test the new target just add from POST above -GET http://127.0.0.1:3334/generate?target=example -HTTP 200 \ No newline at end of file diff --git a/tests/generate_test.go b/tests/generate_test.go deleted file mode 100644 index 21ff978..0000000 --- a/tests/generate_test.go +++ /dev/null @@ -1,418 +0,0 @@ -package tests - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "testing" - - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/generator" - "github.com/OpenCHAMI/configurator/pkg/server" - "github.com/OpenCHAMI/configurator/pkg/util" -) - -var ( - workDir string - replaceDir string - err error -) - -// 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 *config.Config, params generator.Params) (generator.FileMap, error) { - // Jinja 2 template file - files := map[string]generator.Template{ - "test1": generator.Template{ - Contents: []byte(` -Name: {{plugin_name}} -Version: {{plugin_version}} -Description: {{plugin_description}} - -This is the first test template file. - `), - }, - "test2": generator.Template{ - Contents: []byte(` -This is another testing Jinja 2 template file using {{plugin_name}}. - `), - }, - } - - // apply Jinja templates to file - fileMap, 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 have a valid config we can access - if config == nil { - return nil, fmt.Errorf("invalid config (config is nil)") - } - - // TODO: make sure we can get a target - - // make sure we have the same number of files in file list - var ( - fileInputCount = len(files) - fileOutputCount = len(fileMap) - ) - if fileInputCount != fileOutputCount { - return nil, fmt.Errorf("file output count (%d) is not the same as the input (%d)", fileOutputCount, fileInputCount) - } - - return fileMap, nil -} - -func init() { - workDir, err = os.Getwd() - if err != nil { - log.Fatalf("failed to get working directory: %v", err) - } - replaceDir = fmt.Sprintf("%s", filepath.Dir(workDir)) -} - -// 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 ( - "github.com/OpenCHAMI/configurator/pkg/config" - "github.com/OpenCHAMI/configurator/pkg/generator" -) - -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 *config.Config, params generator.Params) (generator.FileMap, error) { - return generator.FileMap{"test": []byte("test")}, nil -} - -var Generator TestGenerator`) - ) - - // get directory to replace remote pkg with local - // _, filename, _, _ := runtime.Caller(0) - // replaceDir := fmt.Sprintf("%s", filepath.Dir(workDir)) - - // show all paths to make sure we're using the correct ones - fmt.Printf("(TestPlugin) working directory: %v\n", workDir) - 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, 0o777) - 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) - } - - // initialize the plugin directory as a Go project - cmd := exec.Command("bash", "-c", "go mod init github.com/OpenCHAMI/configurator-test-plugin") - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - - // use the local `pkg` instead of the release one - cmd = exec.Command("bash", "-c", fmt.Sprintf("go mod edit -replace=github.com/OpenCHAMI/configurator=%s", replaceDir)) - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - - // run `go mod tidy` for dependencies - cmd = exec.Command("bash", "-c", "go mod tidy") - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - - // execute command to build the plugin - cmd = exec.Command("bash", "-c", "go build -buildmode=plugin -o=test-plugin.so test-plugin.go") - if output, err := cmd.CombinedOutput(); 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("test-plugin.so") - 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(*config.Config, generator.Params) (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(*config.Config, generator.Params) (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 - -// An invalid generator that does not or partially implements -// the "Generator" interface. -type InvalidGenerator struct{} -func (g *InvalidGenerator) GetName() string { return "invalid" } -var Generator InvalidGenerator - `) - ) - - // show all paths to make sure we're using the correct ones - fmt.Printf("(TestPluginWithInvalidOrNoSymbol) working directory: %v\n", workDir) - 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) - - // get directory to replace remote pkg with local - // _, filename, _, _ := runtime.Caller(0) - - // 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) - } - - // initialize the plugin directory as a Go project - cmd := exec.Command("bash", "-c", "go mod init github.com/OpenCHAMI/configurator-test-plugin") - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - - // use the local `pkg` instead of the release one - cmd = exec.Command("bash", "-c", fmt.Sprintf("go mod edit -replace=github.com/OpenCHAMI/configurator=%s", replaceDir)) - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - - // run `go mod tidy` for dependencies - cmd = exec.Command("bash", "-c", "go mod tidy") - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to execute command: %v\n%s", err, string(output)) - } - - // execute command to build the plugin - cmd = exec.Command("bash", "-c", "go build -buildmode=plugin -o=invalid-plugin.so invalid-plugin.go") - if output, err := cmd.CombinedOutput(); 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 ( - conf = config.New() - 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 created for running tests." { - t.Error("test generator return unexpected description") - } - }) - - // try to generate a file with templating applied - fileMap, err := gen.Generate(&conf, generator.Params{}) - 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 ( - conf = config.New() - 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. - conf.Targets["test"] = configurator.Target{ - TemplatePaths: []string{}, - FilePaths: []string{}, - } - - // create new server, add test generator, and start in background - server := server.New(&conf) - generator.DefaultGenerators["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(&conf, generator.Params{}) - 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])) - } - } -}