diff --git a/Makefile b/Makefile index ce45665..28c6ed6 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ CURRENT_DIR=$(shell pwd) DIST_DIR=${CURRENT_DIR}/build/dist CLI_NAME=microcks BIN_NAME=microcks +WATCHER_NAME=watcher HOST_OS=$(shell go env GOOS) HOST_ARCH=$(shell go env GOARCH) @@ -23,3 +24,7 @@ build-binaries: make BIN_NAME=${CLI_NAME}-darwin-arm64 GOOS=darwin GOARCH=arm64 build-local make BIN_NAME=${CLI_NAME}-windows-amd64.exe GOOS=windows build-local make BIN_NAME=${CLI_NAME}-windows-386.exe GOOS=windows GOARCH=386 build-local + +.PHONY: build-watcher +build-watcher: + go build -o ${DIST_DIR}/${BIN_NAME}-${WATCHER_NAME} ${PACKAGE}/pkg/importer \ No newline at end of file diff --git a/cmd/context.go b/cmd/context.go index 597cbe9..2be547e 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -63,7 +63,7 @@ microcks context httP://localhost:8080 --delete`, }, } - ctxCmd.Flags().BoolVar(&delete, "delete", false, "Delete a context") + ctxCmd.Flags().BoolVarP(&delete, "delete", "d", false, "Delete a context") return ctxCmd } diff --git a/cmd/import.go b/cmd/import.go index 3ed0e53..8ff7384 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -23,10 +23,13 @@ import ( "github.com/microcks/microcks-cli/pkg/config" "github.com/microcks/microcks-cli/pkg/connectors" + "github.com/microcks/microcks-cli/pkg/errors" "github.com/spf13/cobra" ) func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { + var watch bool + var importCmd = &cobra.Command{ Use: "import", Short: "import API artifacts on Microcks server", @@ -35,6 +38,7 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command // Parse subcommand args first. if len(args) == 0 { fmt.Println("import command require args") + cmd.HelpFunc()(cmd, args) os.Exit(1) } @@ -55,6 +59,10 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command return } + if globalClientOpts.Context == "" { + globalClientOpts.Context = localConfig.CurrentContext + } + mc, err := connectors.NewClient(*globalClientOpts) if err != nil { fmt.Printf("error %v", err) @@ -82,10 +90,30 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command os.Exit(1) } fmt.Printf("Microcks has discovered '%s'\n", msg) + + if watch { + watchFile, err := config.DefaultLocalWatchPath() + errors.CheckError(err) + watchCfg, err := config.ReadLocalWatchConfig(watchFile) + errors.CheckError(err) + if watchCfg == nil { + watchCfg = &config.WatchConfig{} + } + + watchCfg.UpsertEntry(config.WatchEntry{ + FilePath: f, + Context: []string{globalClientOpts.Context}, + MainArtifact: mainArtifact, + }) + //write watch file + err = config.WriteLocalWatchConfig(*watchCfg, watchFile) + errors.CheckError(err) + } } }, } + importCmd.Flags().BoolVar(&watch, "watch", false, "Keep watch on file changes and re-import it on change ") return importCmd } diff --git a/go.mod b/go.mod index bc9305d..2a5c4f1 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/coreos/go-oidc/v3 v3.14.1 github.com/docker/docker v28.0.4+incompatible github.com/docker/go-connections v0.5.0 + github.com/fsnotify/fsnotify v1.9.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index b607189..7c084dd 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/pkg/config/localconfig.go b/pkg/config/localconfig.go index 6289dae..cf75a5f 100644 --- a/pkg/config/localconfig.go +++ b/pkg/config/localconfig.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "slices" configUtil "github.com/microcks/microcks-cli/pkg/util" ) @@ -60,6 +61,16 @@ type Auth struct { ClientSecret string } +type WatchConfig struct { + Entries []WatchEntry `yaml:"entries"` +} + +type WatchEntry struct { + FilePath string `yaml:"filePath"` + Context []string `yaml:"context"` + MainArtifact bool `yaml:"mainartifact"` +} + // ReadLocalConfig loads up the local configuration file. Returns nil if config does not exist func ReadLocalConfig(path string) (*LocalConfig, error) { var err error @@ -123,6 +134,14 @@ func DefaultLocalConfigPath() (string, error) { return path.Join(dir, "config"), nil } +func DefaultLocalWatchPath() (string, error) { + dir, err := DefaultConfigDir() + if err != nil { + return "", err + } + return path.Join(dir, "watch"), nil +} + func ValidateLocalConfig(config LocalConfig) error { if config.CurrentContext == "" { return nil @@ -331,3 +350,45 @@ func (l *LocalConfig) RemoveAuth(server string) bool { } return false } + +func (w *WatchConfig) UpsertEntry(entry WatchEntry) { + for i, e := range w.Entries { + if e.FilePath == entry.FilePath { + contexts := w.Entries[i].Context + if !slices.Contains(contexts, entry.Context[0]) { + entry.Context = append(entry.Context, contexts...) + } + w.Entries[i] = entry + return + } + } + w.Entries = append(w.Entries, entry) +} + +func ReadLocalWatchConfig(path string) (*WatchConfig, error) { + var err error + var config WatchConfig + + // check file permission only when microcks config exists + if fi, err := os.Stat(path); err == nil { + err = getFilePermission(fi) + if err != nil { + return nil, err + } + } + + err = configUtil.UnmarshalLocalFile(path, &config) + if os.IsNotExist(err) { + return nil, nil + } + + return &config, nil +} + +func WriteLocalWatchConfig(config WatchConfig, cfgPath string) error { + err := os.MkdirAll(path.Dir(cfgPath), os.ModePerm) + if err != nil { + return err + } + return configUtil.MarshalLocalYAMLFile(cfgPath, &config) +} diff --git a/pkg/importer/main.go b/pkg/importer/main.go new file mode 100644 index 0000000..b06c5a7 --- /dev/null +++ b/pkg/importer/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + + "github.com/microcks/microcks-cli/pkg/config" + "github.com/microcks/microcks-cli/pkg/errors" + "github.com/microcks/microcks-cli/pkg/importer/watcher" +) + +func main() { + watchFile, err := config.DefaultLocalWatchPath() + errors.CheckError(err) + + wm, err := watcher.NewWatchManger(watchFile) + errors.CheckError(err) + + fmt.Println("[INFO] microcks-watcher started...") + wm.Run() +} diff --git a/pkg/importer/watcher/executor.go b/pkg/importer/watcher/executor.go new file mode 100644 index 0000000..d02848f --- /dev/null +++ b/pkg/importer/watcher/executor.go @@ -0,0 +1,51 @@ +package watcher + +import ( + "fmt" + "strconv" + + "github.com/microcks/microcks-cli/cmd" + "github.com/microcks/microcks-cli/pkg/config" + "github.com/microcks/microcks-cli/pkg/connectors" +) + +func TriggerImport(entry config.WatchEntry) { + mainArtifact := strconv.FormatBool(entry.MainArtifact) + + args := []string{ + entry.FilePath + ":" + mainArtifact, + } + + cfgPath, err := config.DefaultLocalConfigPath() + if err != nil { + fmt.Errorf("Error while loading config: %s", err.Error()) + } + + for _, context := range entry.Context { + importCommand := cmd.NewImportCommand(&connectors.ClientOptions{ + ConfigPath: cfgPath, + Context: context, + }) + importCommand.SetArgs(args) + err = importCommand.Execute() + if err != nil { + fmt.Printf("Error re-importing %s: %v\n", entry.FilePath, err) + } + + fmt.Printf("Imported '%s' in context '%s'\n", entry.FilePath, context) + } +} + +func LoadRegistry(watchFilePath string) (*config.WatchConfig, error) { + var watchCfg *config.WatchConfig + watchCfg, err := config.ReadLocalWatchConfig(watchFilePath) + if err != nil { + return nil, err + } + + if watchCfg == nil { + watchCfg = &config.WatchConfig{} + } + + return watchCfg, nil +} diff --git a/pkg/importer/watcher/watchManager.go b/pkg/importer/watcher/watchManager.go new file mode 100644 index 0000000..47a4a43 --- /dev/null +++ b/pkg/importer/watcher/watchManager.go @@ -0,0 +1,104 @@ +package watcher + +import ( + "fmt" + "log" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/microcks/microcks-cli/pkg/config" + "github.com/microcks/microcks-cli/pkg/errors" +) + +type WatchManager struct { + fileWatcher *fsnotify.Watcher + configPath string + watchEntries map[string]config.WatchEntry + lock sync.Mutex +} + +func NewWatchManger(configPath string) (*WatchManager, error) { + fw, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + err = fw.Add(configPath) + if err != nil { + return nil, err + } + + wm := &WatchManager{ + fileWatcher: fw, + configPath: configPath, + watchEntries: make(map[string]config.WatchEntry), + } + + err = wm.Reload() + if err != nil { + return nil, err + } + + return wm, nil +} + +func (wm *WatchManager) Reload() error { + cfg, err := LoadRegistry(wm.configPath) + if err != nil { + return err + } + + newFiles := map[string]config.WatchEntry{} + for _, entry := range cfg.Entries { + newFiles[entry.FilePath] = entry + } + + // Remove stale watchers + for file := range wm.watchEntries { + if _, exists := newFiles[file]; !exists { + wm.fileWatcher.Remove(file) + } + } + + // Add new watchers + for file := range newFiles { + if _, exists := wm.watchEntries[file]; !exists { + err := wm.fileWatcher.Add(file) + if err != nil { + log.Printf("[WARN] Cannot watch file %s: %v", file, err) + continue + } + } + } + + wm.watchEntries = newFiles + return nil +} + +func (wm *WatchManager) Run() { + for { + select { + case event := <-wm.fileWatcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + if event.Name == wm.configPath { + fmt.Println("[INFO] Reloading config...") + wm.lock.Lock() + err := wm.Reload() + wm.lock.Unlock() + if err != nil { + errors.CheckError(err) + } + } else { + wm.lock.Lock() + entry, exists := wm.watchEntries[event.Name] + wm.lock.Unlock() + if exists { + go TriggerImport(entry) + } + } + } + case err := <-wm.fileWatcher.Errors: + log.Printf("[ERROR] Watcher error: %v", err) + } + } +}