Skip to content

Commit cbf1282

Browse files
authored
Feat: Add 'watch' flag to import command (#162)
* fixes #158 feat: Add 'watch' flag to import command to automatic re-import the local files on changes. Signed-off-by: Harsh4902 <[email protected]> * Adds WATCHER-NAME variable in Makefile and removes unnecessary prints Signed-off-by: Harsh4902 <[email protected]> --------- Signed-off-by: Harsh4902 <[email protected]>
1 parent 1eaa907 commit cbf1282

File tree

9 files changed

+273
-1
lines changed

9 files changed

+273
-1
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CURRENT_DIR=$(shell pwd)
33
DIST_DIR=${CURRENT_DIR}/build/dist
44
CLI_NAME=microcks
55
BIN_NAME=microcks
6+
WATCHER_NAME=watcher
67

78
HOST_OS=$(shell go env GOOS)
89
HOST_ARCH=$(shell go env GOARCH)
@@ -23,3 +24,7 @@ build-binaries:
2324
make BIN_NAME=${CLI_NAME}-darwin-arm64 GOOS=darwin GOARCH=arm64 build-local
2425
make BIN_NAME=${CLI_NAME}-windows-amd64.exe GOOS=windows build-local
2526
make BIN_NAME=${CLI_NAME}-windows-386.exe GOOS=windows GOARCH=386 build-local
27+
28+
.PHONY: build-watcher
29+
build-watcher:
30+
go build -o ${DIST_DIR}/${BIN_NAME}-${WATCHER_NAME} ${PACKAGE}/pkg/importer

cmd/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ microcks context httP://localhost:8080 --delete`,
6363
},
6464
}
6565

66-
ctxCmd.Flags().BoolVar(&delete, "delete", false, "Delete a context")
66+
ctxCmd.Flags().BoolVarP(&delete, "delete", "d", false, "Delete a context")
6767

6868
return ctxCmd
6969
}

cmd/import.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ import (
2323

2424
"github.com/microcks/microcks-cli/pkg/config"
2525
"github.com/microcks/microcks-cli/pkg/connectors"
26+
"github.com/microcks/microcks-cli/pkg/errors"
2627
"github.com/spf13/cobra"
2728
)
2829

2930
func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
31+
var watch bool
32+
3033
var importCmd = &cobra.Command{
3134
Use: "import",
3235
Short: "import API artifacts on Microcks server",
@@ -35,6 +38,7 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command
3538
// Parse subcommand args first.
3639
if len(args) == 0 {
3740
fmt.Println("import command require <specificationFile1[:primary],specificationFile2[:primary]> args")
41+
cmd.HelpFunc()(cmd, args)
3842
os.Exit(1)
3943
}
4044

@@ -55,6 +59,10 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command
5559
return
5660
}
5761

62+
if globalClientOpts.Context == "" {
63+
globalClientOpts.Context = localConfig.CurrentContext
64+
}
65+
5866
mc, err := connectors.NewClient(*globalClientOpts)
5967
if err != nil {
6068
fmt.Printf("error %v", err)
@@ -82,10 +90,30 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command
8290
os.Exit(1)
8391
}
8492
fmt.Printf("Microcks has discovered '%s'\n", msg)
93+
94+
if watch {
95+
watchFile, err := config.DefaultLocalWatchPath()
96+
errors.CheckError(err)
97+
watchCfg, err := config.ReadLocalWatchConfig(watchFile)
98+
errors.CheckError(err)
99+
if watchCfg == nil {
100+
watchCfg = &config.WatchConfig{}
101+
}
102+
103+
watchCfg.UpsertEntry(config.WatchEntry{
104+
FilePath: f,
105+
Context: []string{globalClientOpts.Context},
106+
MainArtifact: mainArtifact,
107+
})
108+
//write watch file
109+
err = config.WriteLocalWatchConfig(*watchCfg, watchFile)
110+
errors.CheckError(err)
111+
}
85112
}
86113

87114
},
88115
}
89116

117+
importCmd.Flags().BoolVar(&watch, "watch", false, "Keep watch on file changes and re-import it on change ")
90118
return importCmd
91119
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/coreos/go-oidc/v3 v3.14.1
99
github.com/docker/docker v28.0.4+incompatible
1010
github.com/docker/go-connections v0.5.0
11+
github.com/fsnotify/fsnotify v1.9.0
1112
github.com/golang-jwt/jwt/v4 v4.5.2
1213
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
1314
github.com/spf13/cobra v1.9.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
2121
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
2222
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
2323
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
24+
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
25+
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
2426
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
2527
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
2628
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=

pkg/config/localconfig.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path"
7+
"slices"
78

89
configUtil "github.com/microcks/microcks-cli/pkg/util"
910
)
@@ -60,6 +61,16 @@ type Auth struct {
6061
ClientSecret string
6162
}
6263

64+
type WatchConfig struct {
65+
Entries []WatchEntry `yaml:"entries"`
66+
}
67+
68+
type WatchEntry struct {
69+
FilePath string `yaml:"filePath"`
70+
Context []string `yaml:"context"`
71+
MainArtifact bool `yaml:"mainartifact"`
72+
}
73+
6374
// ReadLocalConfig loads up the local configuration file. Returns nil if config does not exist
6475
func ReadLocalConfig(path string) (*LocalConfig, error) {
6576
var err error
@@ -123,6 +134,14 @@ func DefaultLocalConfigPath() (string, error) {
123134
return path.Join(dir, "config"), nil
124135
}
125136

137+
func DefaultLocalWatchPath() (string, error) {
138+
dir, err := DefaultConfigDir()
139+
if err != nil {
140+
return "", err
141+
}
142+
return path.Join(dir, "watch"), nil
143+
}
144+
126145
func ValidateLocalConfig(config LocalConfig) error {
127146
if config.CurrentContext == "" {
128147
return nil
@@ -331,3 +350,45 @@ func (l *LocalConfig) RemoveAuth(server string) bool {
331350
}
332351
return false
333352
}
353+
354+
func (w *WatchConfig) UpsertEntry(entry WatchEntry) {
355+
for i, e := range w.Entries {
356+
if e.FilePath == entry.FilePath {
357+
contexts := w.Entries[i].Context
358+
if !slices.Contains(contexts, entry.Context[0]) {
359+
entry.Context = append(entry.Context, contexts...)
360+
}
361+
w.Entries[i] = entry
362+
return
363+
}
364+
}
365+
w.Entries = append(w.Entries, entry)
366+
}
367+
368+
func ReadLocalWatchConfig(path string) (*WatchConfig, error) {
369+
var err error
370+
var config WatchConfig
371+
372+
// check file permission only when microcks config exists
373+
if fi, err := os.Stat(path); err == nil {
374+
err = getFilePermission(fi)
375+
if err != nil {
376+
return nil, err
377+
}
378+
}
379+
380+
err = configUtil.UnmarshalLocalFile(path, &config)
381+
if os.IsNotExist(err) {
382+
return nil, nil
383+
}
384+
385+
return &config, nil
386+
}
387+
388+
func WriteLocalWatchConfig(config WatchConfig, cfgPath string) error {
389+
err := os.MkdirAll(path.Dir(cfgPath), os.ModePerm)
390+
if err != nil {
391+
return err
392+
}
393+
return configUtil.MarshalLocalYAMLFile(cfgPath, &config)
394+
}

pkg/importer/main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/microcks/microcks-cli/pkg/config"
7+
"github.com/microcks/microcks-cli/pkg/errors"
8+
"github.com/microcks/microcks-cli/pkg/importer/watcher"
9+
)
10+
11+
func main() {
12+
watchFile, err := config.DefaultLocalWatchPath()
13+
errors.CheckError(err)
14+
15+
wm, err := watcher.NewWatchManger(watchFile)
16+
errors.CheckError(err)
17+
18+
fmt.Println("[INFO] microcks-watcher started...")
19+
wm.Run()
20+
}

pkg/importer/watcher/executor.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package watcher
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/microcks/microcks-cli/cmd"
8+
"github.com/microcks/microcks-cli/pkg/config"
9+
"github.com/microcks/microcks-cli/pkg/connectors"
10+
)
11+
12+
func TriggerImport(entry config.WatchEntry) {
13+
mainArtifact := strconv.FormatBool(entry.MainArtifact)
14+
15+
args := []string{
16+
entry.FilePath + ":" + mainArtifact,
17+
}
18+
19+
cfgPath, err := config.DefaultLocalConfigPath()
20+
if err != nil {
21+
fmt.Errorf("Error while loading config: %s", err.Error())
22+
}
23+
24+
for _, context := range entry.Context {
25+
importCommand := cmd.NewImportCommand(&connectors.ClientOptions{
26+
ConfigPath: cfgPath,
27+
Context: context,
28+
})
29+
importCommand.SetArgs(args)
30+
err = importCommand.Execute()
31+
if err != nil {
32+
fmt.Printf("Error re-importing %s: %v\n", entry.FilePath, err)
33+
}
34+
35+
fmt.Printf("Imported '%s' in context '%s'\n", entry.FilePath, context)
36+
}
37+
}
38+
39+
func LoadRegistry(watchFilePath string) (*config.WatchConfig, error) {
40+
var watchCfg *config.WatchConfig
41+
watchCfg, err := config.ReadLocalWatchConfig(watchFilePath)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
if watchCfg == nil {
47+
watchCfg = &config.WatchConfig{}
48+
}
49+
50+
return watchCfg, nil
51+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package watcher
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"sync"
7+
8+
"github.com/fsnotify/fsnotify"
9+
"github.com/microcks/microcks-cli/pkg/config"
10+
"github.com/microcks/microcks-cli/pkg/errors"
11+
)
12+
13+
type WatchManager struct {
14+
fileWatcher *fsnotify.Watcher
15+
configPath string
16+
watchEntries map[string]config.WatchEntry
17+
lock sync.Mutex
18+
}
19+
20+
func NewWatchManger(configPath string) (*WatchManager, error) {
21+
fw, err := fsnotify.NewWatcher()
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
err = fw.Add(configPath)
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
wm := &WatchManager{
32+
fileWatcher: fw,
33+
configPath: configPath,
34+
watchEntries: make(map[string]config.WatchEntry),
35+
}
36+
37+
err = wm.Reload()
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
return wm, nil
43+
}
44+
45+
func (wm *WatchManager) Reload() error {
46+
cfg, err := LoadRegistry(wm.configPath)
47+
if err != nil {
48+
return err
49+
}
50+
51+
newFiles := map[string]config.WatchEntry{}
52+
for _, entry := range cfg.Entries {
53+
newFiles[entry.FilePath] = entry
54+
}
55+
56+
// Remove stale watchers
57+
for file := range wm.watchEntries {
58+
if _, exists := newFiles[file]; !exists {
59+
wm.fileWatcher.Remove(file)
60+
}
61+
}
62+
63+
// Add new watchers
64+
for file := range newFiles {
65+
if _, exists := wm.watchEntries[file]; !exists {
66+
err := wm.fileWatcher.Add(file)
67+
if err != nil {
68+
log.Printf("[WARN] Cannot watch file %s: %v", file, err)
69+
continue
70+
}
71+
}
72+
}
73+
74+
wm.watchEntries = newFiles
75+
return nil
76+
}
77+
78+
func (wm *WatchManager) Run() {
79+
for {
80+
select {
81+
case event := <-wm.fileWatcher.Events:
82+
if event.Op&fsnotify.Write == fsnotify.Write {
83+
if event.Name == wm.configPath {
84+
fmt.Println("[INFO] Reloading config...")
85+
wm.lock.Lock()
86+
err := wm.Reload()
87+
wm.lock.Unlock()
88+
if err != nil {
89+
errors.CheckError(err)
90+
}
91+
} else {
92+
wm.lock.Lock()
93+
entry, exists := wm.watchEntries[event.Name]
94+
wm.lock.Unlock()
95+
if exists {
96+
go TriggerImport(entry)
97+
}
98+
}
99+
}
100+
case err := <-wm.fileWatcher.Errors:
101+
log.Printf("[ERROR] Watcher error: %v", err)
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)