Fixed server implementation and refactored

This commit is contained in:
David Allen 2024-06-20 17:09:02 -06:00
parent 0e3eec733b
commit 22195fa00a
No known key found for this signature in database
GPG key ID: 717C593FF60A2ACC
8 changed files with 289 additions and 215 deletions

View file

@ -6,13 +6,10 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"os" "os"
"path/filepath" "path/filepath"
configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator" "github.com/OpenCHAMI/configurator/internal/generator"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -25,28 +22,19 @@ var generateCmd = &cobra.Command{
Use: "generate", Use: "generate",
Short: "Generate a config file from state management", Short: "Generate a config file from state management",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// load generator plugins to generate configs or to print // if we have more than one target and output is set, create configs in directory
var ( targetCount := len(targets)
generators = make(map[string]generator.Generator) if outputPath != "" && targetCount > 1 {
client = configurator.SmdClient{ err := os.MkdirAll(outputPath, 0o755)
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 err != nil { if err != nil {
fmt.Printf("failed to load plugins: %v\n", err) fmt.Printf("failed to make output directory: %v", err)
err = nil os.Exit(1)
continue
} }
}
// add loaded generator plugins to set // use config plugins if none supplied via CLI
maps.Copy(generators, gens) if len(pluginPaths) <= 0 {
pluginPaths = append(pluginPaths, config.PluginDirs...)
} }
// show config as JSON and generators if verbose // show config as JSON and generators if verbose
@ -58,149 +46,47 @@ var generateCmd = &cobra.Command{
fmt.Printf("%v\n", string(b)) fmt.Printf("%v\n", string(b))
} }
// show available targets then exit // generate config with each supplied target
if len(args) == 0 && len(targets) == 0 { for _, target := range targets {
for g := range generators { params := generator.Params{
fmt.Printf("\tplugin: %s, name:\n", g) Args: args,
PluginPaths: pluginPaths,
Target: target,
Verbose: verbose,
} }
os.Exit(0) output, err := generator.Generate(&config, params)
} if err != nil {
fmt.Printf("failed to generate config: %v\n", err)
// make sure that we have a token present before trying to make request os.Exit(1)
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")
}
} }
}
if targets == nil { // write config output if no specific targetPath is set
logrus.Errorf("no target supplied (--target type:template)") if outputPath == "" {
} else { // write only to stdout
// if we have more than one target and output is set, create configs in directory fmt.Printf("%s\n", string(output))
targetCount := len(targets) } else if outputPath != "" && targetCount == 1 {
if outputPath != "" && targetCount > 1 { // write just a single file using template name
err := os.MkdirAll(outputPath, 0o755) err := os.WriteFile(outputPath, output, 0o644)
if err != nil { if err != nil {
logrus.Errorf("failed to make output directory: %v", err) fmt.Printf("failed to write config to file: %v", err)
return 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() { func init() {
generateCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make") generateCmd.Flags().StringSliceVar(&targets, "target", []string{}, "set the target configs to make")
generateCmd.Flags().StringSliceVar(&pluginPaths, "plugin", nil, "set the generator plugins directory path to shared libraries") 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().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") generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token")

View file

@ -4,13 +4,14 @@
package cmd package cmd
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/configurator/internal/server" "github.com/OpenCHAMI/configurator/internal/server"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -18,13 +19,41 @@ var serveCmd = &cobra.Command{
Use: "serve", Use: "serve",
Short: "Start configurator as a server and listen for requests", Short: "Start configurator as a server and listen for requests",
Run: func(cmd *cobra.Command, args []string) { 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 // set up the routes and start the server
server := server.New() server := server.Server{
err := server.Start(&config) 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) { if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("Server closed.") fmt.Printf("Server closed.")
} else if err != nil { } else if err != nil {
logrus.Errorf("failed to start server: %v", err) fmt.Errorf("failed to start server: %v", err)
os.Exit(1) os.Exit(1)
} }
}, },
@ -33,7 +62,8 @@ var serveCmd = &cobra.Command{
func init() { func init() {
serveCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the server host") 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().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().StringSliceVar(&pluginPaths, "plugins", nil, "set the generator plugins directory path")
serveCmd.Flags().IntVar(&config.Options.JwksRetries, "jwks-fetch-retries", config.Options.JwksRetries, "set the JWKS fetch retry count") 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) rootCmd.AddCommand(serveCmd)
} }

View file

@ -11,7 +11,7 @@ import (
) )
type SmdClient struct { type SmdClient struct {
http.Client http.Client `json:"-"`
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
AccessToken string `yaml:"access-token"` AccessToken string `yaml:"access-token"`
@ -20,7 +20,7 @@ type SmdClient struct {
type Params = map[string]any type Params = map[string]any
type Option func(Params) type Option func(Params)
func WithVerbose() Option { func WithVerbosity() Option {
return func(p util.Params) { return func(p util.Params) {
p["verbose"] = true 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 // 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. // service SMD_JWKS_URL envirnoment variable is set.
func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]EthernetInterface, error) { 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 // make request to SMD endpoint
b, err := client.makeRequest("/Inventory/EthernetInterfaces") b, err := client.makeRequest("/Inventory/EthernetInterfaces")
if err != nil { if err != nil {
@ -42,16 +47,14 @@ func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]Etherne
} }
// unmarshal response body JSON and extract in object // unmarshal response body JSON and extract in object
eths := []EthernetInterface{} // []map[string]any{}
err = json.Unmarshal(b, &eths) err = json.Unmarshal(b, &eths)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err) return nil, fmt.Errorf("failed to unmarshal response: %v", err)
} }
// print what we got if verbose is set // print what we got if verbose is set
params := util.GetParams(opts...) if verbose != nil {
if verbose, ok := params["verbose"].(bool); ok { if *verbose {
if verbose {
fmt.Printf("Ethernet Interfaces: %v\n", string(b)) 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 // 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. // service SMD_JWKS_URL envirnoment variable is set.
func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, error) { 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 // make request to SMD endpoint
b, err := client.makeRequest("/State/Components") b, err := client.makeRequest("/State/Components")
if err != nil { 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 // unmarshal response body JSON and extract in object
comps := []Component{}
err = json.Unmarshal(b, &comps) err = json.Unmarshal(b, &comps)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err) return nil, fmt.Errorf("failed to unmarshal response: %v", err)
} }
// print what we got if verbose is set // print what we got if verbose is set
params := util.GetParams(opts...) if verbose != nil {
if verbose, ok := params["verbose"].(bool); ok { if *verbose {
if verbose {
fmt.Printf("Components: %v\n", string(b)) fmt.Printf("Components: %v\n", string(b))
} }
} }
@ -86,6 +92,32 @@ func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, erro
return comps, nil 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) { func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) {
if client == nil { if client == nil {
return nil, fmt.Errorf("client is nil") return nil, fmt.Errorf("client is nil")

View file

@ -27,7 +27,7 @@ type Config struct {
SmdClient SmdClient `yaml:"smd"` SmdClient SmdClient `yaml:"smd"`
AccessToken string `yaml:"access-token"` AccessToken string `yaml:"access-token"`
TemplatePaths map[string]string `yaml:"templates"` TemplatePaths map[string]string `yaml:"templates"`
Plugins []string `yaml:"plugins"` PluginDirs []string `yaml:"plugins"`
Options Options `yaml:"options"` Options Options `yaml:"options"`
} }
@ -45,7 +45,7 @@ func NewConfig() Config {
"powerman": "templates/powerman.jinja", "powerman": "templates/powerman.jinja",
"conman": "templates/conman.jinja", "conman": "templates/conman.jinja",
}, },
Plugins: []string{}, PluginDirs: []string{},
Server: Server{ Server: Server{
Host: "127.0.0.1", Host: "127.0.0.1",
Port: 3334, Port: 3334,

View file

@ -1,5 +1,7 @@
package configurator package configurator
import "encoding/json"
type IPAddr struct { type IPAddr struct {
IpAddress string `json:"IPAddress"` IpAddress string `json:"IPAddress"`
Network string `json:"Network"` Network string `json:"Network"`
@ -16,6 +18,38 @@ type EthernetInterface struct {
} }
type Component 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 { type Node struct {

View file

@ -3,6 +3,7 @@ package generator
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"maps"
"os" "os"
"plugin" "plugin"
@ -10,6 +11,7 @@ import (
"github.com/OpenCHAMI/configurator/internal/util" "github.com/OpenCHAMI/configurator/internal/util"
"github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2"
"github.com/nikolalohinski/gonja/v2/exec" "github.com/nikolalohinski/gonja/v2/exec"
"github.com/sirupsen/logrus"
) )
type Mappings = map[string]any type Mappings = map[string]any
@ -19,6 +21,13 @@ type Generator interface {
Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) 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) { func LoadPlugin(path string) (Generator, error) {
p, err := plugin.Open(path) p, err := plugin.Open(path)
if err != nil { if err != nil {
@ -45,53 +54,33 @@ func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, err
) )
items, _ := os.ReadDir(dirpath) 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 { for _, item := range items {
if item.IsDir() { if item.IsDir() {
subitems, _ := os.ReadDir(item.Name()) subitems, _ := os.ReadDir(item.Name())
for _, subitem := range subitems { for _, subitem := range subitems {
if !subitem.IsDir() { if !subitem.IsDir() {
gen, err := LoadGenerator(subitem.Name()) gen, err := LoadPlugin(subitem.Name())
if err != nil { if err != nil {
fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err) fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err)
continue continue
} }
if verbose, ok := params["verbose"].(bool); ok { if verbose, ok := params["verbose"].(bool); ok {
if verbose { if verbose {
fmt.Printf("found plugin '%s'\n", item.Name()) fmt.Printf("-- found plugin '%s'\n", item.Name())
} }
} }
gens[gen.GetName()] = gen gens[gen.GetName()] = gen
} }
} }
} else { } else {
gen, err := LoadGenerator(dirpath + item.Name()) gen, err := LoadPlugin(dirpath + item.Name())
if err != nil { if err != nil {
fmt.Printf("failed to load generator: %v\n", err) fmt.Printf("failed to load generator: %v\n", err)
continue continue
} }
if verbose, ok := params["verbose"].(bool); ok { if verbose, ok := params["verbose"].(bool); ok {
if verbose { if verbose {
fmt.Printf("found plugin '%s'\n", dirpath+item.Name()) fmt.Printf("-- found plugin '%s'\n", dirpath+item.Name())
} }
} }
gens[gen.GetName()] = gen 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 WithOption(key string, value any) util.Option {
func Get[T any](params util.Params, key string) *T { return func(p util.Params) {
if v, ok := params[key].(T); ok { p[key] = value
return &v
} }
return nil
} }
// Helper function to get client in generator plugins. // Helper function to get client in generator plugins.
func GetClient(params util.Params) *configurator.SmdClient { 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 { func GetParams(opts ...util.Option) util.Params {
@ -144,10 +131,6 @@ func GetParams(opts ...util.Option) util.Params {
return 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) { func ApplyTemplate(path string, mappings map[string]any) ([]byte, error) {
data := exec.NewContext(mappings) data := exec.NewContext(mappings)
@ -165,3 +148,71 @@ func ApplyTemplate(path string, mappings map[string]any) ([]byte, error) {
return b.Bytes(), nil 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")
}

View file

@ -9,6 +9,7 @@ import (
"time" "time"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/jwtauth/v5" "github.com/OpenCHAMI/jwtauth/v5"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@ -19,9 +20,15 @@ var (
tokenAuth *jwtauth.JWTAuth = nil tokenAuth *jwtauth.JWTAuth = nil
) )
type Jwks struct {
Uri string
Retries int
}
type Server struct { type Server struct {
*http.Server *http.Server
JwksUri string `yaml:"jwks-uri"` Jwks Jwks `yaml:"jwks"`
GeneratorParams generator.Params
TokenAuth *jwtauth.JWTAuth
} }
func New() *Server { func New() *Server {
@ -29,11 +36,14 @@ func New() *Server {
Server: &http.Server{ Server: &http.Server{
Addr: "localhost:3334", 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 // create client just for the server to use to fetch data from SMD
_ = &configurator.SmdClient{ _ = &configurator.SmdClient{
Host: config.SmdClient.Host, 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 // create new go-chi router with its routes
router := chi.NewRouter() router := chi.NewRouter()
router.Use(middleware.RedirectSlashes) router.Use(middleware.RedirectSlashes)
@ -67,11 +83,19 @@ func (s *Server) Start(config *configurator.Config) error {
jwtauth.Authenticator(tokenAuth), jwtauth.Authenticator(tokenAuth),
) )
} }
r.HandleFunc("/target", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/generate", func(w http.ResponseWriter, r *http.Request) {
// g := generator.Generator{ s.GeneratorParams.Target = r.URL.Query().Get("target")
// Type: r.URL.Query().Get("type"), output, err := generator.Generate(config, s.GeneratorParams)
// Template: r.URL.Query().Get("template"), 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 // NOTE: we probably don't want to hardcode the types, but should do for now
// if _type == "dhcp" { // if _type == "dhcp" {

View file

@ -35,3 +35,20 @@ func AssertOptionsExist(params Params, opts ...string) []string {
} }
return foundKeys 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
}