diff --git a/cmd/generate.go b/cmd/generate.go index d73d645..d9b7fa0 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -6,13 +6,10 @@ package cmd import ( "encoding/json" "fmt" - "maps" "os" "path/filepath" - configurator "github.com/OpenCHAMI/configurator/internal" "github.com/OpenCHAMI/configurator/internal/generator" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -25,28 +22,19 @@ var generateCmd = &cobra.Command{ Use: "generate", Short: "Generate a config file from state management", Run: func(cmd *cobra.Command, args []string) { - // load generator plugins to generate configs or to print - var ( - generators = make(map[string]generator.Generator) - client = configurator.SmdClient{ - Host: config.SmdClient.Host, - Port: config.SmdClient.Port, - AccessToken: config.AccessToken, - } - ) - for _, path := range pluginPaths { - if verbose { - fmt.Printf("loading plugins from '%s'\n", path) - } - gens, err := generator.LoadPlugins(path) + // if we have more than one target and output is set, create configs in directory + targetCount := len(targets) + if outputPath != "" && targetCount > 1 { + err := os.MkdirAll(outputPath, 0o755) if err != nil { - fmt.Printf("failed to load plugins: %v\n", err) - err = nil - continue + fmt.Printf("failed to make output directory: %v", err) + os.Exit(1) } + } - // add loaded generator plugins to set - maps.Copy(generators, gens) + // use config plugins if none supplied via CLI + if len(pluginPaths) <= 0 { + pluginPaths = append(pluginPaths, config.PluginDirs...) } // show config as JSON and generators if verbose @@ -58,149 +46,47 @@ var generateCmd = &cobra.Command{ fmt.Printf("%v\n", string(b)) } - // show available targets then exit - if len(args) == 0 && len(targets) == 0 { - for g := range generators { - fmt.Printf("\tplugin: %s, name:\n", g) + // generate config with each supplied target + for _, target := range targets { + params := generator.Params{ + Args: args, + PluginPaths: pluginPaths, + Target: target, + Verbose: verbose, } - os.Exit(0) - } - - // make sure that we have a token present before trying to make request - if config.AccessToken == "" { - // TODO: make request to check if request will need token - - // 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 != "" { - config.AccessToken = accessToken - } else { - // TODO: try and fetch token first if it is needed - if verbose { - fmt.Printf("No token found. Attempting to generate config without one...\n") - } + output, err := generator.Generate(&config, params) + if err != nil { + fmt.Printf("failed to generate config: %v\n", err) + os.Exit(1) } - } - if targets == nil { - logrus.Errorf("no target supplied (--target type:template)") - } else { - // if we have more than one target and output is set, create configs in directory - targetCount := len(targets) - if outputPath != "" && targetCount > 1 { - err := os.MkdirAll(outputPath, 0o755) + // write config output if no specific targetPath is set + if outputPath == "" { + // write only to stdout + fmt.Printf("%s\n", string(output)) + } else if outputPath != "" && targetCount == 1 { + // write just a single file using template name + err := os.WriteFile(outputPath, output, 0o644) if err != nil { - logrus.Errorf("failed to make output directory: %v", err) - return + fmt.Printf("failed to write config to file: %v", err) + os.Exit(1) + } + } else if outputPath != "" && targetCount > 1 { + // write multiple files in directory using template name + err := os.WriteFile(fmt.Sprintf("%s/%s.%s", filepath.Clean(outputPath), target, ".conf"), output, 0o644) + if err != nil { + fmt.Printf("failed to write config to file: %v", err) + os.Exit(1) } } - - for _, target := range targets { - // split the target and type - // tmp := strings.Split(target, ":") - - // make sure each target has at least two args - // if len(tmp) < 2 { - // message := "target" - // if len(tmp) == 1 { - // message += fmt.Sprintf(" '%s'", tmp[0]) - // } - // message += " does not provide enough arguments (args: \"type:template\")" - // logrus.Errorf(message) - // continue - // } - // var ( - // _type = tmp[0] - // _template = tmp[1] - // ) - // g := generator.Generator{ - // Type: tmp[0], - // Template: tmp[1], - // } - - // check if another param is specified - // targetPath := "" - // if len(tmp) > 2 { - // targetPath = tmp[2] - // } - - // run the generator plugin from target passed - gen := generators[target] - if gen == nil { - fmt.Printf("invalid generator target (%s)\n", target) - continue - } - output, err := gen.Generate( - &config, - generator.WithTemplate(gen.GetName()), - generator.WithClient(client), - ) - if err != nil { - fmt.Printf("failed to generate config: %v\n", err) - continue - } - - // NOTE: we probably don't want to hardcode the types, but should do for now - // ext := "" - // contents := []byte{} - // if _type == "dhcp" { - // // fetch eths from SMD - // eths, err := client.FetchEthernetInterfaces() - // if err != nil { - // logrus.Errorf("failed to fetch DHCP metadata: %v\n", err) - // continue - // } - // if len(eths) <= 0 { - // continue - // } - // // generate a new config from that data - // contents, err = g.GenerateDHCP(&config, eths) - // if err != nil { - // logrus.Errorf("failed to generate DHCP config file: %v\n", err) - // continue - // } - // ext = "conf" - // } else if g.Type == "dns" { - // // TODO: fetch from SMD - // // TODO: generate config from pulled info - - // } else if g.Type == "syslog" { - - // } else if g.Type == "ansible" { - - // } else if g.Type == "warewulf" { - - // } - - // write config output if no specific targetPath is set - // if targetPath == "" { - if outputPath == "" { - // write only to stdout - fmt.Printf("%s\n", string(output)) - } else if outputPath != "" && targetCount == 1 { - // write just a single file using template name - err := os.WriteFile(outputPath, output, 0o644) - if err != nil { - logrus.Errorf("failed to write config to file: %v", err) - continue - } - } else if outputPath != "" && targetCount > 1 { - // write multiple files in directory using template name - err := os.WriteFile(fmt.Sprintf("%s/%s.%s", filepath.Clean(outputPath), target, ".conf"), output, 0o644) - if err != nil { - logrus.Errorf("failed to write config to file: %v", err) - continue - } - } - // } - } // for targets } + }, } func init() { - generateCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make") - generateCmd.Flags().StringSliceVar(&pluginPaths, "plugin", nil, "set the generator plugins directory path to shared libraries") + generateCmd.Flags().StringSliceVar(&targets, "target", []string{}, "set the target configs to make") + generateCmd.Flags().StringSliceVar(&pluginPaths, "plugins", []string{}, "set the generator plugins directory path") generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets") generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token") diff --git a/cmd/serve.go b/cmd/serve.go index d60ad9f..998ff36 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -4,13 +4,14 @@ package cmd import ( + "encoding/json" "errors" "fmt" "net/http" "os" + "github.com/OpenCHAMI/configurator/internal/generator" "github.com/OpenCHAMI/configurator/internal/server" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -18,13 +19,41 @@ var serveCmd = &cobra.Command{ Use: "serve", Short: "Start configurator as a server and listen for requests", Run: func(cmd *cobra.Command, args []string) { + // use config plugins if none supplied via CLI + if len(pluginPaths) <= 0 { + pluginPaths = append(pluginPaths, config.PluginDirs...) + } + + // show config as JSON and generators if verbose + if verbose { + b, err := json.MarshalIndent(config, "", " ") + if err != nil { + fmt.Printf("failed to marshal config: %v\n", err) + } + fmt.Printf("%v\n", string(b)) + } + // set up the routes and start the server - server := server.New() - err := server.Start(&config) + server := server.Server{ + Server: &http.Server{ + Addr: fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port), + }, + Jwks: server.Jwks{ + Uri: config.Server.Jwks.Uri, + Retries: config.Server.Jwks.Retries, + }, + GeneratorParams: generator.Params{ + Args: args, + PluginPaths: pluginPaths, + // Target: target, // NOTE: targets are set via HTTP requests (ex: curl http://configurator:3334/generate?target=dnsmasq) + Verbose: verbose, + }, + } + err := server.Serve(&config) if errors.Is(err, http.ErrServerClosed) { fmt.Printf("Server closed.") } else if err != nil { - logrus.Errorf("failed to start server: %v", err) + fmt.Errorf("failed to start server: %v", err) os.Exit(1) } }, @@ -33,7 +62,8 @@ var serveCmd = &cobra.Command{ func init() { serveCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the server host") serveCmd.Flags().IntVar(&config.Server.Port, "port", config.Server.Port, "set the server port") - serveCmd.Flags().StringVar(&config.Options.JwksUri, "jwks-uri", config.Options.JwksUri, "set the JWKS url to fetch public key") - serveCmd.Flags().IntVar(&config.Options.JwksRetries, "jwks-fetch-retries", config.Options.JwksRetries, "set the JWKS fetch retry count") + serveCmd.Flags().StringSliceVar(&pluginPaths, "plugins", nil, "set the generator plugins directory path") + serveCmd.Flags().StringVar(&config.Server.Jwks.Uri, "jwks-uri", config.Server.Jwks.Uri, "set the JWKS url to fetch public key") + serveCmd.Flags().IntVar(&config.Server.Jwks.Retries, "jwks-fetch-retries", config.Server.Jwks.Retries, "set the JWKS fetch retry count") rootCmd.AddCommand(serveCmd) } diff --git a/internal/client.go b/internal/client.go index 68038d9..4b750c6 100644 --- a/internal/client.go +++ b/internal/client.go @@ -11,7 +11,7 @@ import ( ) type SmdClient struct { - http.Client + http.Client `json:"-"` Host string `yaml:"host"` Port int `yaml:"port"` AccessToken string `yaml:"access-token"` @@ -20,7 +20,7 @@ type SmdClient struct { type Params = map[string]any type Option func(Params) -func WithVerbose() Option { +func WithVerbosity() Option { return func(p util.Params) { p["verbose"] = true } @@ -35,6 +35,11 @@ func NewParams() Params { // 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(opts ...util.Option) ([]EthernetInterface, error) { + var ( + params = util.GetParams(opts...) + verbose = util.Get[bool](params, "verbose") + eths = []EthernetInterface{} + ) // make request to SMD endpoint b, err := client.makeRequest("/Inventory/EthernetInterfaces") if err != nil { @@ -42,16 +47,14 @@ func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]Etherne } // unmarshal response body JSON and extract in object - eths := []EthernetInterface{} // []map[string]any{} err = json.Unmarshal(b, ðs) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %v", err) } // print what we got if verbose is set - params := util.GetParams(opts...) - if verbose, ok := params["verbose"].(bool); ok { - if verbose { + if verbose != nil { + if *verbose { fmt.Printf("Ethernet Interfaces: %v\n", string(b)) } } @@ -62,23 +65,26 @@ func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]Etherne // 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(opts ...util.Option) ([]Component, error) { + var ( + params = util.GetParams(opts...) + verbose = util.Get[bool](params, "verbose") + comps = []Component{} + ) // make request to SMD endpoint b, err := client.makeRequest("/State/Components") if err != nil { - return nil, fmt.Errorf("failed to read HTTP response: %v", err) + return nil, fmt.Errorf("failed to make HTTP request: %v", err) } // unmarshal response body JSON and extract in object - comps := []Component{} err = json.Unmarshal(b, &comps) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %v", err) } // print what we got if verbose is set - params := util.GetParams(opts...) - if verbose, ok := params["verbose"].(bool); ok { - if verbose { + if verbose != nil { + if *verbose { fmt.Printf("Components: %v\n", string(b)) } } @@ -86,6 +92,32 @@ func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, erro return comps, nil } +func (client *SmdClient) FetchRedfishEndpoints(opts ...util.Option) ([]RedfishEndpoint, error) { + var ( + params = util.GetParams(opts...) + verbose = util.Get[bool](params, "verbose") + rfs = []RedfishEndpoint{} + ) + + b, err := client.makeRequest("/Inventory/RedfishEndpoints") + if err != nil { + return nil, fmt.Errorf("failed to make HTTP resquest: %v", err) + } + + err = json.Unmarshal(b, &rfs) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %v", err) + } + + if verbose != nil { + if *verbose { + fmt.Printf("Redfish endpoints: %v\n", string(b)) + } + } + + return rfs, nil +} + func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) { if client == nil { return nil, fmt.Errorf("client is nil") diff --git a/internal/config.go b/internal/config.go index 7aabb0a..f8cdaa4 100644 --- a/internal/config.go +++ b/internal/config.go @@ -27,7 +27,7 @@ type Config struct { SmdClient SmdClient `yaml:"smd"` AccessToken string `yaml:"access-token"` TemplatePaths map[string]string `yaml:"templates"` - Plugins []string `yaml:"plugins"` + PluginDirs []string `yaml:"plugins"` Options Options `yaml:"options"` } @@ -45,7 +45,7 @@ func NewConfig() Config { "powerman": "templates/powerman.jinja", "conman": "templates/conman.jinja", }, - Plugins: []string{}, + PluginDirs: []string{}, Server: Server{ Host: "127.0.0.1", Port: 3334, diff --git a/internal/configurator.go b/internal/configurator.go index a20d5e3..68dae99 100644 --- a/internal/configurator.go +++ b/internal/configurator.go @@ -1,5 +1,7 @@ package configurator +import "encoding/json" + type IPAddr struct { IpAddress string `json:"IPAddress"` Network string `json:"Network"` @@ -16,6 +18,38 @@ type EthernetInterface struct { } 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 { diff --git a/internal/generator/generator.go b/internal/generator/generator.go index fabfe0c..ef41281 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -3,6 +3,7 @@ package generator import ( "bytes" "fmt" + "maps" "os" "plugin" @@ -10,6 +11,7 @@ import ( "github.com/OpenCHAMI/configurator/internal/util" "github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2/exec" + "github.com/sirupsen/logrus" ) type Mappings = map[string]any @@ -19,6 +21,13 @@ type Generator interface { Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) } +type Params struct { + Args []string + PluginPaths []string + Target string + Verbose bool +} + func LoadPlugin(path string) (Generator, error) { p, err := plugin.Open(path) if err != nil { @@ -45,53 +54,33 @@ func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, err ) items, _ := os.ReadDir(dirpath) - var LoadGenerator = func(path string) (Generator, error) { - // load each generator plugin - p, err := plugin.Open(path) - if err != nil { - return nil, fmt.Errorf("failed to load plugin: %v", err) - } - - // lookup symbol in plugin - symbol, err := p.Lookup("Generator") - if err != nil { - return nil, fmt.Errorf("failed to look up symbol: %v", err) - } - - // assert that the loaded symbol is the correct type - gen, ok := symbol.(Generator) - if !ok { - return nil, fmt.Errorf("failed to load the correct symbol type") - } - return gen, nil - } for _, item := range items { if item.IsDir() { subitems, _ := os.ReadDir(item.Name()) for _, subitem := range subitems { if !subitem.IsDir() { - gen, err := LoadGenerator(subitem.Name()) + gen, err := LoadPlugin(subitem.Name()) if err != nil { fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err) continue } if verbose, ok := params["verbose"].(bool); ok { if verbose { - fmt.Printf("found plugin '%s'\n", item.Name()) + fmt.Printf("-- found plugin '%s'\n", item.Name()) } } gens[gen.GetName()] = gen } } } else { - gen, err := LoadGenerator(dirpath + item.Name()) + gen, err := LoadPlugin(dirpath + item.Name()) if err != nil { fmt.Printf("failed to load generator: %v\n", err) continue } if verbose, ok := params["verbose"].(bool); ok { if verbose { - fmt.Printf("found plugin '%s'\n", dirpath+item.Name()) + fmt.Printf("-- found plugin '%s'\n", dirpath+item.Name()) } } gens[gen.GetName()] = gen @@ -123,17 +112,15 @@ func WithClient(client configurator.SmdClient) util.Option { } } -// Syntactic sugar generic function to get parameter from util.Params. -func Get[T any](params util.Params, key string) *T { - if v, ok := params[key].(T); ok { - return &v +func WithOption(key string, value any) util.Option { + return func(p util.Params) { + p[key] = value } - return nil } // Helper function to get client in generator plugins. func GetClient(params util.Params) *configurator.SmdClient { - return Get[configurator.SmdClient](params, "client") + return util.Get[configurator.SmdClient](params, "client") } func GetParams(opts ...util.Option) util.Params { @@ -144,10 +131,6 @@ func GetParams(opts ...util.Option) util.Params { return params } -func Generate(g Generator, config *configurator.Config, opts ...util.Option) { - g.Generate(config, opts...) -} - func ApplyTemplate(path string, mappings map[string]any) ([]byte, error) { data := exec.NewContext(mappings) @@ -165,3 +148,71 @@ func ApplyTemplate(path string, mappings map[string]any) ([]byte, error) { return b.Bytes(), nil } + +func Generate(config *configurator.Config, params Params) ([]byte, error) { + // load generator plugins to generate configs or to print + var ( + generators = make(map[string]Generator) + client = configurator.SmdClient{ + Host: config.SmdClient.Host, + Port: config.SmdClient.Port, + AccessToken: config.AccessToken, + } + ) + + // make sure that we have a token present before trying to make request + if config.AccessToken == "" { + // TODO: make request to check if request will need token + + // 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 != "" { + config.AccessToken = accessToken + } else { + // TODO: try and fetch token first if it is needed + if params.Verbose { + fmt.Printf("No token found. Attempting to generate config without one...\n") + } + } + } + + // load all plugins from params + for _, path := range params.PluginPaths { + if params.Verbose { + fmt.Printf("loading plugins from '%s'\n", path) + } + gens, err := LoadPlugins(path) + if err != nil { + fmt.Printf("failed to load plugins: %v\n", err) + err = nil + continue + } + + // add loaded generator plugins to set + maps.Copy(generators, gens) + } + + // show available targets then exit + if len(params.Args) == 0 && params.Target == "" { + for g := range generators { + fmt.Printf("-- found generator plugin \"%s\"\n", g) + } + 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, + WithTemplate(gen.GetName()), + WithClient(client), + ) + } + return nil, fmt.Errorf("an unknown error has occurred") +} diff --git a/internal/server/server.go b/internal/server/server.go index 5f57073..b5ce16b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,6 +9,7 @@ import ( "time" configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/generator" "github.com/OpenCHAMI/jwtauth/v5" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -19,9 +20,15 @@ var ( tokenAuth *jwtauth.JWTAuth = nil ) +type Jwks struct { + Uri string + Retries int +} type Server struct { *http.Server - JwksUri string `yaml:"jwks-uri"` + Jwks Jwks `yaml:"jwks"` + GeneratorParams generator.Params + TokenAuth *jwtauth.JWTAuth } func New() *Server { @@ -29,11 +36,14 @@ func New() *Server { Server: &http.Server{ Addr: "localhost:3334", }, - JwksUri: "", + Jwks: Jwks{ + Uri: "", + Retries: 5, + }, } } -func (s *Server) Start(config *configurator.Config) error { +func (s *Server) Serve(config *configurator.Config) error { // create client just for the server to use to fetch data from SMD _ = &configurator.SmdClient{ Host: config.SmdClient.Host, @@ -56,6 +66,12 @@ func (s *Server) Start(config *configurator.Config) error { } } + var WriteError = func(w http.ResponseWriter, format string, a ...any) { + errmsg := fmt.Sprintf(format, a...) + fmt.Printf(errmsg) + w.Write([]byte(errmsg)) + } + // create new go-chi router with its routes router := chi.NewRouter() router.Use(middleware.RedirectSlashes) @@ -67,11 +83,19 @@ func (s *Server) Start(config *configurator.Config) error { jwtauth.Authenticator(tokenAuth), ) } - r.HandleFunc("/target", func(w http.ResponseWriter, r *http.Request) { - // g := generator.Generator{ - // Type: r.URL.Query().Get("type"), - // Template: r.URL.Query().Get("template"), - // } + r.HandleFunc("/generate", func(w http.ResponseWriter, r *http.Request) { + s.GeneratorParams.Target = r.URL.Query().Get("target") + output, err := generator.Generate(config, s.GeneratorParams) + if err != nil { + WriteError(w, "failed to generate config: %v\n", err) + return + } + + _, err = w.Write(output) + if err != nil { + WriteError(w, "failed to write response: %v", err) + return + } // NOTE: we probably don't want to hardcode the types, but should do for now // if _type == "dhcp" { diff --git a/internal/util/params.go b/internal/util/params.go index cc006fc..2417083 100644 --- a/internal/util/params.go +++ b/internal/util/params.go @@ -35,3 +35,20 @@ func AssertOptionsExist(params Params, opts ...string) []string { } return foundKeys } + +func WithDefault[T any](v T) Option { + return func(p Params) { + p["default"] = v + } +} + +// Syntactic sugar generic function to get parameter from util.Params. +func Get[T any](params Params, key string, opts ...Option) *T { + if v, ok := params[key].(T); ok { + return &v + } + if defaultValue, ok := params["default"].(T); ok { + return &defaultValue + } + return nil +}