From 4d55a3edc21b2a6382a9dbdc264b5b1e601a537a Mon Sep 17 00:00:00 2001 From: David Allen Date: Fri, 5 Sep 2025 22:21:27 -0600 Subject: [PATCH] feat: initial implementation of run cmd --- cmd/run.go | 277 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 275 insertions(+), 2 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index ab698e7..846e560 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,6 +1,22 @@ package cmd -import "github.com/spf13/cobra" +import ( + "fmt" + "io/fs" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "git.towk2.me/towk/makeshift/internal/archive" + makeshift "git.towk2.me/towk/makeshift/pkg" + "git.towk2.me/towk/makeshift/pkg/service" + "git.towk2.me/towk/makeshift/pkg/storage" + "git.towk2.me/towk/makeshift/pkg/util" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) var runCmd = &cobra.Command{ Use: "run", @@ -13,16 +29,273 @@ var runCmd = &cobra.Command{ export MAKESHIFT_ROOT=/opt/makeshift # run locally similar to 'download' - makeshift run --plugins jinja2 --profiles default + makeshift run -p help.txt --plugins jinja2 --profiles default makeshift run --root $HOME/apps/makeshift -p help.txt --plugins jinja2 --profiles default `, Args: cobra.NoArgs, Short: "Run locally with plugins and profiles", Run: func(cmd *cobra.Command, args []string) { + var ( + host, _ = cmd.Flags().GetString("host") + path, _ = cmd.Flags().GetString("path") + rootPath, _ = cmd.Flags().GetString("root") + outputPath, _ = cmd.Flags().GetString("output") + cacertPath, _ = cmd.Flags().GetString("cacert") + keyfile, _ = cmd.Flags().GetString("keyfile") + timeout, _ = cmd.Flags().GetInt("timeout") + pluginNames, _ = cmd.Flags().GetStringSlice("plugins") + profileIDs, _ = cmd.Flags().GetStringSlice("profiles") + extract, _ = cmd.Flags().GetBool("extract") + removeArchive, _ = cmd.Flags().GetBool("remove-archive") + contents []byte + parsed *url.URL + localServer *service.Service + fileInfo os.FileInfo + out *os.File + store *storage.MemoryStorage = new(storage.MemoryStorage) + hooks []makeshift.Hook + errs []error + err error + ) + + // 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 + localServer = service.New() + localServer.Addr = parsed.Host + localServer.RootPath = rootPath + localServer.CACertFile = cacertPath + localServer.CACertKeyfile = keyfile + localServer.Timeout = time.Duration(timeout) * time.Second + + // initialize storage + store.Init() + store.SetKWArgs(&pluginKWArgs) + + // prepare the profiles + errs = localServer.LoadProfiles(profileIDs, store, errs) + if len(errs) > 0 { + log.Error(). + Errs("errs", errs). + Msg("errors occurred loading profiles") + err = util.FormatErrors("failed to load plugins", "", errs) + errs = []error{} + } + + // prepare the plugins + // determine if path is directory, file, or exists + if fileInfo, err = os.Stat(path); err == nil { + if fileInfo.IsDir() { + // get the final archive path + archivePath := fmt.Sprintf("%s.tar.gz", path) + + log.Debug(). + Str("archive_path", archivePath). + Str("type", "directory"). + Msg("Service.Download()") + + out, err = os.Create(archivePath) + if err != nil { + log.Error(). + Err(err). + Str("path", archivePath). + Msg("failed to create named file") + return + } + + // get a list of filenames to archive + filenamesToArchive := []string{} + filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { + if !d.IsDir() { + filenamesToArchive = append(filenamesToArchive, path) + } + return nil + }) + log.Debug().Strs("files", filenamesToArchive).Send() + + // prepare plugins + hooks, errs = localServer.LoadPlugins(pluginNames, store, pluginArgs, errs) + if len(errs) > 0 { + log.Error(). + Errs("errs", errs). + Strs("plugins", pluginNames). + Strs("args", pluginArgs). + Msg("errors occurred loading plugins") + errs = []error{} + return + } + + // create an archive of the directory, run hooks, and download + err = archive.Create(filenamesToArchive, out, hooks) + if err != nil { + log.Error(). + Err(err). + Str("path", archivePath). + Msg("failed to create archive") + return + } + + // load the final archive + contents, err = os.ReadFile(archivePath) + if err != nil { + log.Error(). + Err(err). + Str("path", archivePath). + Msg("failed to read archive contents") + return + } + + // clean up the temporary archive + err = os.Remove(archivePath) + if err != nil { + log.Error().Err(err).Msg("failed to remove temporary archive") + return + } + + // extract files if '-x' flag is passed + if extract { + var ( + dir = filepath.Dir(outputPath) + base = strings.TrimSuffix(filepath.Base(outputPath), ".tar.gz") + ) + err = archive.Expand(outputPath, fmt.Sprintf("%s/%s", dir, base)) + if err != nil { + log.Error().Err(err). + Str("path", outputPath). + Msg("failed to expand archive") + os.Exit(1) + } + } + + // optionally, remove archive if '-r' flag is passed + // NOTE: this can only be used if `-x` flag is set + if removeArchive { + if !extract { + log.Warn().Msg("requires '-x/--extract' flag to be set to 'true'") + } else { + err = os.Remove(outputPath) + if err != nil { + log.Error().Err(err). + Str("path", outputPath). + Msg("failed to remove archive") + } + } + } + + } else { + // download individual file + log.Debug(). + Str("type", "file"). + Msg("Service.Download()") + + contents, err = os.ReadFile(path) + if err != nil { + log.Error(). + Err(err). + Msg("failed to read file to download") + return + } + + // prepare plugins + store.Set("file", contents) + hooks, errs = localServer.LoadPlugins(pluginNames, store, pluginArgs, errs) + if len(errs) > 0 { + log.Error(). + Strs("plugins", pluginNames). + Strs("args", pluginArgs). + Errs("errs", errs). + Msg("errors occurred loading plugins") + errs = []error{} + return + } + if len(hooks) > 0 { + // run pre-hooks to modify the contents of the file before archiving + log.Debug().Int("hook_count", len(hooks)).Msg("running hooks") + for _, hook := range hooks { + log.Debug().Any("hook", map[string]any{ + "store": hook.Data, + "args": hook.Args, + "plugin": map[string]string{ + "name": hook.Plugin.Name(), + "description": hook.Plugin.Description(), + "version": hook.Plugin.Version(), + }, + }).Send() + err = hook.Init() + if err != nil { + log.Error(). + Err(err). + Str("plugin", hook.Plugin.Name()). + Msg("failed to initialize plugin") + continue + } + err = hook.Run() + if err != nil { + log.Error(). + Err(err). + Str("plugin", hook.Plugin.Name()). + Msg("failed to run plugin") + continue + } + err = hook.Cleanup() + if err != nil { + log.Error(). + Err(err). + Str("plugin", hook.Plugin.Name()). + Msg("failed to cleanup plugin") + continue + } + } + + // take the contents from the last hook and update files + var ( + hook = hooks[len(hooks)-1] + data any + ) + data, err = hook.Data.Get("out") + if err != nil { + log.Error(). + Err(err). + Str("plugin", hook.Plugin.Name()). + Msg("failed to get data from hook") + return + } + + // write to file if '-o' specified otherwise stdout + if outputPath != "" { + writeFiles(outputPath, data.([]byte)) + log.Debug().Str("path", outputPath).Msg("wrote file to specified path") + } else { + fmt.Println(string(data.([]byte))) + } + + } else { + // write contents to file + // send non-processed file back as response + + } + } + } }, } func init() { + runCmd.PersistentFlags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)") + runCmd.PersistentFlags().StringP("output", "o", "", "Set the output path to write files") + runCmd.PersistentFlags().String("cacert", "", "Set the CA certificate path to load") + runCmd.Flags().StringP("path", "p", ".", "Set the path to list files (can be set with MAKESHIFT_PATH)") + runCmd.Flags().StringSlice("profiles", []string{}, "Set the profile(s) to use to populate data store") + runCmd.Flags().StringSlice("plugins", []string{}, "Set the plugin(s) to run before downloading files") + runCmd.Flags().StringSlice("plugin-args", []string{}, "Set the argument list to pass to plugin(s)") + runCmd.Flags().Var(&pluginKWArgs, "plugin-kwargs", "Set the argument map to pass to plugin(s)") + runCmd.Flags().BoolP("extract", "x", false, "Set whether to extract archive locally after downloading") + runCmd.Flags().BoolP("remove-archive", "r", false, "Set whether to remove the archive after extracting (used with '--extract' flag)") rootCmd.AddCommand(runCmd) }