diff --git a/pkg/server/server.go b/pkg/server/server.go index 9afc7db..cd448cc 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -4,16 +4,16 @@ package server import ( - "crypto/tls" - "crypto/x509" "encoding/json" "fmt" - "net" + "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" @@ -36,62 +36,43 @@ type Jwks struct { } type Server struct { *http.Server - Config *configurator.Config + 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(config *configurator.Config) *Server { +func New(conf *config.Config) *Server { // create default config if none supplied - if config == nil { - c := configurator.NewConfig() - config = &c + if conf == nil { + c := config.New() + conf = &c } // return based on config values return &Server{ - Config: config, + Config: conf, Server: &http.Server{ - Addr: fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port), + Addr: fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port), }, Jwks: Jwks{ - Uri: config.Server.Jwks.Uri, - Retries: config.Server.Jwks.Retries, + Uri: conf.Server.Jwks.Uri, + Retries: conf.Server.Jwks.Retries, }, } } // Main function to start up configurator as a service. -func (s *Server) Serve(cacertPath string) error { - // create client just for the server to use to fetch data from SMD - client := &configurator.SmdClient{ - Host: s.Config.SmdClient.Host, - Port: s.Config.SmdClient.Port, - } - - // add cert to client if `--cacert` flag is passed - if cacertPath != "" { - cacert, _ := os.ReadFile(cacertPath) - certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(cacert) - client.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, - } - } - +func (s *Server) Serve() error { // set the server address with config values - s.Server.Addr = fmt.Sprintf("%s:%d", s.Config.Server.Host, s.Config.Server.Port) + s.Server.Addr = s.Config.Server.Host // fetch JWKS public key from authorization server if s.Config.Server.Jwks.Uri != "" && tokenAuth == nil { @@ -110,6 +91,12 @@ func (s *Server) Serve(cacertPath string) error { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // create client with opts to use to fetch data from SMD + opts := []client.Option{ + 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) @@ -127,13 +114,13 @@ func (s *Server) Serve(cacertPath string) error { ) // protected routes if using auth - r.HandleFunc("/generate", s.Generate(client)) - r.HandleFunc("/templates", s.ManageTemplates) + r.HandleFunc("/generate", s.Generate(opts...)) + r.Post("/targets", s.createTarget) }) } else { // public routes without auth - router.HandleFunc("/generate", s.Generate(client)) - router.HandleFunc("/templates", s.ManageTemplates) + router.HandleFunc("/generate", s.Generate(opts...)) + router.Post("/targets", s.createTarget) } // always available public routes go here (none at the moment) @@ -150,22 +137,37 @@ 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(client *configurator.SmdClient) func(w http.ResponseWriter, r *http.Request) { - +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 - s.GeneratorParams.Target = r.URL.Query().Get("target") - s.GeneratorParams.Client = client - if s.GeneratorParams.Target == "" { + var ( + target string = r.URL.Query().Get("target") + ) + s.GeneratorParams = parseGeneratorParams(r, opts...) + if target == "" { writeErrorResponse(w, "must specify a target") return } - // generate a new config file from supplied params - outputs, err := generator.GenerateWithTarget(s.Config, s.GeneratorParams) - if err != nil { - writeErrorResponse(w, "failed to generate file: %v", err) - return + // try to generate with target supplied by client first + var ( + t *Target = s.getTarget(target) + outputs generator.FileMap + err error + ) + + if t != nil { + outputs, err = generator.Generate(t.PluginPath, s.GeneratorParams) + if err != nil { + + } + } else { + // try and generate a new config file from supplied params + outputs, err = generator.GenerateWithTarget(s.Config, target) + if err != nil { + writeErrorResponse(w, "failed to generate file: %v", err) + return + } } // marshal output to JSON then send response to client @@ -183,22 +185,75 @@ func (s *Server) Generate(client *configurator.SmdClient) func(w http.ResponseWr } } -// Incomplete WIP function for managing templates remotely. There is currently no -// internal API to do this yet. +// 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) ManageTemplates(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte("this is not implemented yet")) - if err != nil { - writeErrorResponse(w, "failed to write response: %v", err) +func (s *Server) createTarget(w http.ResponseWriter, r *http.Request) { + var ( + target = Target{} + bytes []byte + err error + ) + if r == nil { + writeErrorResponse(w, "request is invalid") return } + + bytes, err = io.ReadAll(r.Body) + if err != nil { + writeErrorResponse(w, "failed to read response body: %v", err) + return + } + defer r.Body.Close() + + err = json.Unmarshal(bytes, &target) + if err != nil { + writeErrorResponse(w, "failed to unmarshal target: %v", err) + return + } + + // make sure a plugin and at least one template is supplied + if target.Name == "" { + writeErrorResponse(w, "target name is required") + return + } + if target.PluginPath == "" { + writeErrorResponse(w, "must supply a generator name") + return + } + if len(target.Templates) <= 0 { + writeErrorResponse(w, "must provided at least one template") + return + } + + s.Targets[target.Name] = target + +} + +func (s *Server) getTarget(target string) *Target { + t, ok := s.Targets[target] + 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...) - w.Write([]byte(errmsg)) + log.Error().Msg(errmsg) + http.Error(w, errmsg, http.StatusInternalServerError) return fmt.Errorf(errmsg) } + +func parseGeneratorParams(r *http.Request, opts ...client.Option) generator.Params { + var params = generator.Params{ + ClientOpts: opts, + } + return params +}