From 59a5225b28914d1b981e23a579efbfae7c32a0e4 Mon Sep 17 00:00:00 2001 From: David Allen Date: Sun, 24 Aug 2025 20:39:39 -0600 Subject: [PATCH] feat: updated implementations for cmds --- cmd/download.go | 97 ++++++++++++++++++++---------- cmd/list.go | 28 ++++++--- cmd/plugins.go | 157 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 90 ++++++++++++++------------- cmd/run.go | 2 +- cmd/serve.go | 49 +++++++++------ cmd/upload.go | 10 +++ 7 files changed, 331 insertions(+), 102 deletions(-) create mode 100644 cmd/plugins.go diff --git a/cmd/download.go b/cmd/download.go index 5f787df..b431920 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -3,11 +3,13 @@ package cmd import ( "fmt" "net/http" + "net/url" "os" "path/filepath" + "strings" "time" - "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/makeshift/pkg/client" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -15,30 +17,40 @@ import ( var downloadCmd = cobra.Command{ Use: "download", Example: ` - # download a file or directory (as archive) - configurator download - configurator download --host https://example.com --path test - configurator download --plugins smd,jinja2 --profile compute - curl $CONFIGURATOR_HOST/download/test?plugins=smd,jinja2 + # set up environment + export MAKESHIFT_HOST=http://localhost:5050 + export MAKESHIFT_PATH=test + + # download a file or directory (as archive) + makeshift download + makeshift download --host http://localhost:5050.com --path test + + # download a file or directory and run plugins with profile data + makeshift download --plugins smd,jinja2 --profile compute + curl $MAKESHIFT_HOST/download/test?plugins=smd,jinja2&profile=test + + # download directory and extract it's contents automatically + makeshift download --extract `, Short: "Download and modify files with plugins", PreRun: func(cmd *cobra.Command, args []string) { - setenv(&host, "CONFIGURATOR_HOST") - setenv(&path, "CONFIGURATOR_PATH") + setenv(cmd, "host", "MAKESHIFT_HOST") + setenv(cmd, "path", "MAKESHIFT_PATH") }, Run: func(cmd *cobra.Command, args []string) { var ( - c = client.New(host) - res *http.Response - body []byte - err error - ) + host, _ = cmd.Flags().GetString("host") + path, _ = cmd.Flags().GetString("path") + outputPath, _ = cmd.Flags().GetString("output") + pluginNames, _ = cmd.Flags().GetStringSlice("plugins") + profileIDs, _ = cmd.Flags().GetStringSlice("profiles") - log.Debug(). - Str("host", host). - Str("path", path). - Str("output", outputPath). - Send() + c = client.New(host) + res *http.Response + query string + body []byte + err error + ) // set output path to match path if empty if outputPath == "" { @@ -49,22 +61,42 @@ var downloadCmd = cobra.Command{ } } - // make request to /download endpoint - // _, err = c.Download(outputPath, client.HTTPEnvelope{ - // Path: fmt.Sprintf("/download/%s", path), - // Method: http.MethodGet, - // }) + query = fmt.Sprintf("/download/%s?", path) + if len(pluginNames) > 0 { + query += "plugins=" + url.QueryEscape(strings.Join(pluginNames, ",")) + } + if len(profileIDs) > 0 { + query += "&profiles=" + url.QueryEscape(strings.Join(profileIDs, ",")) + } + + log.Debug(). + Str("host", host). + Str("path", path). + Str("query", query). + Str("output", outputPath). + Strs("profiles", profileIDs). + Strs("plugins", pluginNames). + Send() res, body, err = c.MakeRequest(client.HTTPEnvelope{ - Path: fmt.Sprintf("/download/%s", path), + Path: query, Method: http.MethodGet, }) if err != nil { - log.Error().Err(err).Str("host", host).Msg("failed to make request") + log.Error().Err(err). + Str("host", host). + Str("path", path). + Str("output", outputPath). + Msg("failed to make request") os.Exit(1) } if res.StatusCode != http.StatusOK { - log.Error().Int("status", res.StatusCode).Str("host", host).Msg("response returned bad status") + log.Error(). + Int("status", res.StatusCode). + Str("host", host). + Str("path", path). + Str("output", outputPath). + Msg("response returned bad status") os.Exit(1) } if outputPath != "" { @@ -80,12 +112,15 @@ var downloadCmd = cobra.Command{ }, } +var downloadProfileCmd = &cobra.Command{} +var downloadPluginCmd = &cobra.Command{} + func init() { - downloadCmd.Flags().StringVar(&host, "host", "http://localhost:5050", "Set the configurator remote host (can be set with CONFIGURATOR_HOST)") - downloadCmd.Flags().StringVarP(&path, "path", "p", ".", "Set the path to list files (can be set with CONFIGURATOR_PATH)") - downloadCmd.Flags().StringVarP(&outputPath, "output", "o", "", "Set the output path to write files") - downloadCmd.Flags().StringVar(&profile, "profile", "", "Set the profile to use to populate data store") - downloadCmd.Flags().StringSliceVar(&plugins, "plugins", []string{}, "Set the plugins to run before downloading files") + downloadCmd.Flags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)") + downloadCmd.Flags().StringP("path", "p", ".", "Set the path to list files (can be set with MAKESHIFT_PATH)") + downloadCmd.Flags().StringP("output", "o", "", "Set the output path to write files") + downloadCmd.Flags().StringSlice("profiles", []string{}, "Set the profile to use to populate data store") + downloadCmd.Flags().StringSlice("plugins", []string{}, "Set the plugins to run before downloading files") rootCmd.AddCommand(&downloadCmd) } diff --git a/cmd/list.go b/cmd/list.go index f350a43..38cc145 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -6,7 +6,7 @@ import ( "net/http" "os" - "git.towk2.me/towk/configurator/pkg/client" + "git.towk2.me/towk/makeshift/pkg/client" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -14,19 +14,24 @@ import ( var listCmd = &cobra.Command{ Use: "list", Example: ` - # list files in a remote data directory - configurator list --path test - configurator list --host https://example.com --path test - curl https://example.com/list/test + # list files in a remote data directory + configurator list --path test + configurator list --host http://localhost:5050 --path test + + # list files using 'curl' + curl http://localhost:5050/list/test `, Args: cobra.NoArgs, Short: "List all files in a remote data directory", PreRun: func(cmd *cobra.Command, args []string) { - setenv(&host, "CONFIGURATOR_HOST") - setenv(&path, "CONFIGURATOR_PATH") + setenv(cmd, "host", "MAKESHIFT_HOST") + setenv(cmd, "path", "MAKESHIFT_PATH") }, Run: func(cmd *cobra.Command, args []string) { var ( + host, _ = cmd.Flags().GetString("host") + path, _ = cmd.Flags().GetString("path") + c = client.New(host) body []byte output []string @@ -44,7 +49,10 @@ var listCmd = &cobra.Command{ Method: http.MethodGet, }) if err != nil { - log.Error().Err(err).Str("url", host).Msg("failed to make request") + log.Error().Err(err). + Str("host", host). + Str("path", path). + Msg("failed to make request") os.Exit(1) } @@ -60,8 +68,8 @@ var listCmd = &cobra.Command{ } func init() { - listCmd.Flags().StringVar(&host, "host", "http://localhost:5050", "Set the configurator remote host (can be set with CONFIGURATOR_HOST)") - listCmd.Flags().StringVarP(&path, "path", "p", ".", "Set the path to list files (can be set with CONFIGURATOR_PATH)") + listCmd.Flags().String("host", "http://localhost:5050", "Set the configurator remote host (can be set with MAKESHIFT_HOST)") + listCmd.Flags().StringP("path", "p", ".", "Set the path to list files (can be set with MAKESHIFT_PATH)") rootCmd.AddCommand(listCmd) } diff --git a/cmd/plugins.go b/cmd/plugins.go new file mode 100644 index 0000000..3078e43 --- /dev/null +++ b/cmd/plugins.go @@ -0,0 +1,157 @@ +package cmd + +import ( + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + + makeshift "git.towk2.me/towk/makeshift/pkg" + "git.towk2.me/towk/makeshift/pkg/service" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var pluginsCmd = &cobra.Command{ + Use: "plugins", + Short: "Manage and compile plugins (requires Go build tools)", +} + +var pluginsCompileCmd = &cobra.Command{ + Use: "compile", + Example: ` + # compile plugin using Go build tools + go build -buildmode=plugin -o lib/myplugin.so src/plugins/myplugin.go + + # try to compile all plugins in current directory + cd src/plugins + makeshift plugin compile + + # try to compile all plugins in specified directory + makeshift plugin compile src/plugins + + # compile 'src/plugins/myplugin.go' and save to 'lib/myplugin.so' + makeshift plugin compile src/plugins/myplugin.go -o lib/myplugin.so +`, + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var ( + outputPath, _ = cmd.Flags().GetString("output") + output []byte + fileInfo os.FileInfo + err error + ) + + // make the directory + err = os.MkdirAll(filepath.Dir(outputPath), 0o777) + if err != nil { + log.Fatal().Err(err).Msg("failed to make output directory") + } + + // one arg passed, so determine if it is file or directory + if len(args) > 0 { + if fileInfo, err = os.Stat(args[0]); err == nil { + if fileInfo.IsDir() { + err = compilePluginsDir(args[0], outputPath) + if err != nil { + log.Fatal().Err(err). + Bytes("output", output). + Msg("failed to compile plugin") + } + } else { + // not a directory so check if Go file so try and compile it + if filepath.Ext(args[0]) == ".go" { + output, err = compilePlugin(outputPath, args[0]) + if err != nil { + log.Fatal().Err(err). + Bytes("output", output). + Msg("failed to compile plugin") + } + } else { + log.Fatal().Msg("argument is not a valid plugin (must be Go file)") + } + } + } else if err != nil { + log.Fatal().Err(err).Msgf("failed to stat provided plugin path") + } + } else { + // no args passed, so use current directory + err = compilePluginsDir(".", outputPath) + if err != nil { + log.Fatal().Err(err). + Bytes("output", output). + Msg("failed to compile plugin") + } + } + }, +} + +var pluginInspectCmd = &cobra.Command{ + Use: "inspect", + Args: cobra.MinimumNArgs(1), + Example: ` + # inspect a plugin and print its information + makeshift plugin inspect lib/jinja2.so +`, + Run: func(cmd *cobra.Command, args []string) { + for _, path := range args { + var ( + plugin makeshift.Plugin + err error + ) + plugin, err = service.LoadPluginFromFile(path) + if err != nil { + log.Error().Err(err). + Str("path", path). + Msg("failed to load plugin from file") + continue + } + + log.Info().Any("plugin", map[string]any{ + "name": plugin.Name(), + "version": plugin.Version(), + "description": plugin.Description(), + "metadata": plugin.Metadata(), + }).Send() + } + }, +} + +func init() { + pluginsCompileCmd.Flags().StringP("output", "o", "", "Set the path to save compiled plugin") + pluginsCmd.AddCommand(pluginsCompileCmd, pluginInspectCmd) + rootCmd.AddCommand(pluginsCmd) +} + +func compilePlugin(outputPath string, srcPath string) ([]byte, error) { + var ( + commandArgs string + command *exec.Cmd + ) + // execute command to build the plugin + commandArgs = fmt.Sprintf("go build -buildmode=plugin -o=%s %s", outputPath, srcPath) + command = exec.Command("bash", "-c", commandArgs) + return command.CombinedOutput() +} + +func compilePluginsDir(dirpath string, outputPath string) error { + err := filepath.WalkDir(dirpath, func(path string, d fs.DirEntry, err error) error { + // not a directory and is Go file, so try and compile it + if !d.IsDir() && filepath.Ext(path) == ".go" { + var ( + localOutputPath string = outputPath + "/" + path + ) + output, err := compilePlugin(localOutputPath, path) + if err != nil { + log.Fatal().Err(err). + Bytes("output", output). + Str("path", localOutputPath). + Msg("failed to compile plugin") + return err + } + } + return nil + }) + return err +} diff --git a/cmd/root.go b/cmd/root.go index 40ecd25..375f4bc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,36 +3,58 @@ package cmd import ( "fmt" "os" - "slices" - "strings" - "github.com/rs/zerolog" + logger "git.towk2.me/towk/makeshift/pkg/log" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) var ( - host string - path string - outputPath string - rootPath string - logLevel string - profile string - plugins []string - timeout int + // host string + // path string + // outputPath string + // rootPath string + // profile string + // plugins []string + // timeout int + // logFile string + logLevel logger.LogLevel = logger.INFO ) var rootCmd = cobra.Command{ - Use: "configurator", - Short: "Extensible configuration builder to download files", + Use: "makeshift", + Short: "Extensible file cobbler", PersistentPreRun: func(cmd *cobra.Command, args []string) { - // set the logging level - level, err := strToLogLevel(logLevel) + var ( + logFile string + err error + ) + + // initialize the logger + logFile, _ = cmd.Flags().GetString("log-file") + err = logger.InitWithLogLevel(logLevel, logFile) if err != nil { - log.Error().Err(err).Msg("failed to convert log level argument") + log.Error().Err(err).Msg("failed to initialize logger") os.Exit(1) } - zerolog.SetGlobalLevel(level) + }, + Run: func(cmd *cobra.Command, args []string) { + // try and set flags using env vars + setenv(cmd, "log-file", "MAKESHIFT_LOG_FILE") + if len(args) == 0 { + err := cmd.Help() + if err != nil { + log.Error().Err(err).Msg("failed to print help") + } + os.Exit(0) + } + }, + PostRun: func(cmd *cobra.Command, args []string) { + log.Debug().Msg("closing log file") + err := logger.LogFile.Close() + if err != nil { + log.Error().Err(err).Msg("failed to close log file") + } }, } @@ -46,34 +68,16 @@ func Execute() { func init() { // initialize the config a single time - rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "Set the log level output") + rootCmd.PersistentFlags().VarP(&logLevel, "log-level", "l", "Set the log level output") + rootCmd.PersistentFlags().String("log-file", "", "Set the log file path (can be set with MAKESHIFT_LOG_FILE)") } -func strToLogLevel(ll string) (zerolog.Level, error) { - levels := []string{"debug", "info", "warn", "disabled"} - if index := slices.Index(levels, ll); index >= 0 { - // handle special case to map index == 3 to zerolog.Disabled == 7 - switch index { - case 3: - return zerolog.Disabled, nil - } - return zerolog.Level(index), nil +func setenv(cmd *cobra.Command, varname string, envvar string) { + if cmd.Flags().Changed(varname) { + return } - return -100, fmt.Errorf( - "invalid log level (options: %s)", strings.Join(levels, ", "), - ) // use 'info' by default -} - -func setenv(v *string, key string) { - t := os.Getenv(key) - if t != "" { - *v = t + val := os.Getenv(envvar) + if val != "" { + cmd.Flags().Set(varname, val) } } - -// func setenv(cmd *cobra.Command, varname string, envvar string) { -// v := os.Getenv(envvar) -// if v != "" { -// cmd.Flags().Set(varname, v) -// } -// } diff --git a/cmd/run.go b/cmd/run.go index 369700c..f932e07 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -4,7 +4,7 @@ import "github.com/spf13/cobra" var runCmd = &cobra.Command{ Use: "run", - Short: "Run the configurator locally", + Short: "Run 'makeshift' locally with plugins", Run: func(cmd *cobra.Command, args []string) { }, diff --git a/cmd/serve.go b/cmd/serve.go index 0358149..252a085 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,42 +1,57 @@ package cmd import ( + "net/url" "time" - "git.towk2.me/towk/configurator/pkg/service" + "git.towk2.me/towk/makeshift/pkg/service" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) var serveCmd = &cobra.Command{ - Use: "serve", + Use: "serve", + Example: ` + # start the service in current directory + makeshift serve + + # start the service with root path and initialize + makeshift serve --root ./test --init -l debug +`, Args: cobra.NoArgs, PreRun: func(cmd *cobra.Command, args []string) { - // try and set flags using env vars - setenv(&host, "CONFIGURATOR_HOST") - setenv(&rootPath, "CONFIGURATOR_ROOT") + setenv(cmd, "host", "MAKESHIFT_HOST") + setenv(cmd, "root", "MAKESHIFT_SERVER_ROOT") + setenv(cmd, "timeout", "MAKESHIFT_TIMEOUT") }, Run: func(cmd *cobra.Command, args []string) { var ( - host string - rootPath string - server *service.Service - err error + host, _ = cmd.Flags().GetString("host") + rootPath, _ = cmd.Flags().GetString("root") + timeout, _ = cmd.Flags().GetInt("timeout") + + parsed *url.URL + server *service.Service + err error ) - // get vars but don't modify - host, _ = cmd.Flags().GetString("host") - rootPath, _ = cmd.Flags().GetString("root") + // parse the host to remove scheme if needed + parsed, err = url.Parse(host) + if err != nil { + log.Warn().Err(err). + Str("host", host). + Msg("could not parse host") + } // set the server values server = service.New() - server.Addr = host + server.Addr = parsed.Host server.RootPath = rootPath server.Timeout = time.Duration(timeout) * time.Second // show some debugging information log.Debug(). - Str("host", host). + Str("host", parsed.Host). Any("paths", map[string]string{ "root": rootPath, "data": server.PathForData(), @@ -59,9 +74,9 @@ var serveCmd = &cobra.Command{ func init() { serveCmd.Flags().Bool("init", false, "Initializes default files at specified with the '--root' flag") - serveCmd.Flags().StringVar(&host, "host", "localhost:5050", "Set the configurator server host (can be set with CONFIGURATOR_HOST)") - serveCmd.Flags().StringVar(&rootPath, "root", "./", "Set the root path to serve files (can be set with CONFIGURATOR_ROOT)") - serveCmd.Flags().IntVarP(&timeout, "timeout", "t", 60, "Set the timeout in seconds for requests.") + serveCmd.Flags().String("host", "localhost:5050", "Set the configurator server host (can be set with MAKESHIFT_HOST)") + serveCmd.Flags().String("root", "./", "Set the root path to serve files (can be set with MAKESHIFT_ROOT)") + serveCmd.Flags().IntP("timeout", "t", 60, "Set the timeout in seconds for requests (can be set with MAKESHIFT_TIMEOUT)") rootCmd.AddCommand(serveCmd) } diff --git a/cmd/upload.go b/cmd/upload.go index 9fc1f12..39aa49f 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -9,6 +9,16 @@ var uploadCmd = &cobra.Command{ }, } +var uploadProfileCmd = &cobra.Command{ + Use: "profile", +} + +var uploadPluginCmd = &cobra.Command{ + Use: "plugin", +} + func init() { + + uploadCmd.AddCommand(uploadProfileCmd, uploadPluginCmd) rootCmd.AddCommand(uploadCmd) }