refactor: initial commit for major rewrite

This commit is contained in:
David Allen 2025-08-03 20:25:18 -06:00
parent 3253cb8bbb
commit bfd83f35a3
Signed by: towk
GPG key ID: 0430CDBE22619155
45 changed files with 439 additions and 1733 deletions

View file

@ -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)
}

7
cmd/download.go Normal file
View file

@ -0,0 +1,7 @@
package cmd
import "github.com/spf13/cobra"
var downloadCmd = cobra.Command{
Use: "download",
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

0
cmd/list.go Normal file
View file

0
cmd/profiles.go Normal file
View file

0
cmd/render.go Normal file
View file

View file

@ -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
}
}

View file

@ -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)
}

14
cmd/upload.go Normal file
View file

@ -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)
}