From f9189f82d4f42c705db6d43fef6d38f00e1ef6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joana=20Hrotk=C3=B3?= Date: Wed, 12 Feb 2025 18:04:59 +0000 Subject: [PATCH 1/3] Add multiple paths for rebuild and restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joana Hrotkó --- loader/loader_test.go | 13 +-- loader/validate.go | 10 ++- loader/validate_test.go | 171 ++++++++++++++++++++++++--------------- schema/compose-spec.json | 2 +- types/develop.go | 2 +- validation/validation.go | 15 +++- 6 files changed, 137 insertions(+), 76 deletions(-) diff --git a/loader/loader_test.go b/loader/loader_test.go index c0c75656..2a4a8de8 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -3073,8 +3073,11 @@ services: develop: watch: # rebuild image and recreate service - - path: ./backend/src - action: rebuild + - action: rebuild + path: + - ./backend/src + - ./backend + proxy: image: example/proxy build: ./proxy @@ -3094,7 +3097,7 @@ services: assert.DeepEqual(t, *frontend.Develop, types.DevelopConfig{ Watch: []types.Trigger{ { - Path: "./webapp/html", + Path: []string{"./webapp/html"}, Action: types.WatchActionSync, Target: "/var/www", Ignore: []string{"node_modules/"}, @@ -3109,7 +3112,7 @@ services: assert.DeepEqual(t, *backend.Develop, types.DevelopConfig{ Watch: []types.Trigger{ { - Path: "./backend/src", + Path: []string{"./backend/src", "./backend"}, Action: types.WatchActionRebuild, }, }, @@ -3119,7 +3122,7 @@ services: assert.DeepEqual(t, *proxy.Develop, types.DevelopConfig{ Watch: []types.Trigger{ { - Path: "./proxy/proxy.conf", + Path: []string{"./proxy/proxy.conf"}, Action: types.WatchActionSyncRestart, Target: "/etc/nginx/proxy.conf", }, diff --git a/loader/validate.go b/loader/validate.go index aa570888..943447c3 100644 --- a/loader/validate.go +++ b/loader/validate.go @@ -167,8 +167,14 @@ func checkConsistency(project *types.Project) error { //nolint:gocyclo if s.Develop != nil && s.Develop.Watch != nil { for _, watch := range s.Develop.Watch { - if watch.Target == "" && watch.Action != types.WatchActionRebuild && watch.Action != types.WatchActionRestart { - return fmt.Errorf("services.%s.develop.watch: target is required for non-rebuild actions: %w", s.Name, errdefs.ErrInvalid) + if watch.Action != types.WatchActionRebuild && watch.Action != types.WatchActionRestart { + if watch.Target == "" { + return fmt.Errorf("services.%s.develop.watch: target is required for %s, %s and %s actions: %w", s.Name, types.WatchActionSync, types.WatchActionSyncExec, types.WatchActionSyncRestart, errdefs.ErrInvalid) + + } + if len(watch.Path) > 1 { + return fmt.Errorf("services.%s.develop.watch: can only use more than one path for actions %s and %s: %w", s.Name, types.WatchActionRebuild, types.WatchActionRestart, errdefs.ErrInvalid) + } } } } diff --git a/loader/validate_test.go b/loader/validate_test.go index 5c1aa958..9c08a0ba 100644 --- a/loader/validate_test.go +++ b/loader/validate_test.go @@ -17,6 +17,7 @@ package loader import ( + "fmt" "strings" "testing" @@ -291,7 +292,7 @@ func TestValidateWatch(t *testing.T) { Watch: []types.Trigger{ { Action: types.WatchActionSync, - Path: "/app", + Path: []string{"/app"}, Target: "/container/app", }, }, @@ -303,69 +304,6 @@ func TestValidateWatch(t *testing.T) { assert.NilError(t, err) }) - t.Run("watch missing target for sync action", func(t *testing.T) { - project := types.Project{ - Services: types.Services{ - "myservice": { - Name: "myservice", - Image: "scratch", - Develop: &types.DevelopConfig{ - Watch: []types.Trigger{ - { - Action: types.WatchActionSync, - Path: "/app", - }, - }, - }, - }, - }, - } - err := checkConsistency(&project) - assert.Error(t, err, "services.myservice.develop.watch: target is required for non-rebuild actions: invalid compose project") - }) - - t.Run("watch missing target for sync+restart action", func(t *testing.T) { - project := types.Project{ - Services: types.Services{ - "myservice": { - Name: "myservice", - Image: "scratch", - Develop: &types.DevelopConfig{ - Watch: []types.Trigger{ - { - Action: types.WatchActionSyncRestart, - Path: "/app", - }, - }, - }, - }, - }, - } - err := checkConsistency(&project) - assert.Error(t, err, "services.myservice.develop.watch: target is required for non-rebuild actions: invalid compose project") - }) - - t.Run("watch config valid with missing target for rebuild action", func(t *testing.T) { - project := types.Project{ - Services: types.Services{ - "myservice": { - Name: "myservice", - Image: "scratch", - Develop: &types.DevelopConfig{ - Watch: []types.Trigger{ - { - Action: types.WatchActionRebuild, - Path: "/app", - }, - }, - }, - }, - }, - } - err := checkConsistency(&project) - assert.NilError(t, err) - }) - t.Run("depends on disabled service", func(t *testing.T) { project := types.Project{ Services: types.Services{ @@ -406,4 +344,109 @@ func TestValidateWatch(t *testing.T) { err := checkConsistency(&project) assert.ErrorContains(t, err, "depends on undefined service") }) + + type WatchActionTest struct { + action types.WatchAction + } + tests := []WatchActionTest{ + {action: types.WatchActionSync}, + {action: types.WatchActionSyncRestart}, + {action: types.WatchActionSyncExec}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("watch config is INVALID when missing target for %s action", tt.action), func(t *testing.T) { + project := types.Project{ + Services: types.Services{ + "myservice": { + Name: "myservice", + Image: "scratch", + Develop: &types.DevelopConfig{ + Watch: []types.Trigger{ + { + Action: tt.action, + Path: []string{"/app"}, + // Missing Target + }, + }, + }, + }, + }, + } + err := checkConsistency(&project) + assert.Error(t, err, "services.myservice.develop.watch: target is required for sync, sync+exec and sync+restart actions: invalid compose project") + }) + + t.Run(fmt.Sprintf("watch config is INVALID with one or more paths for %s action", tt.action), func(t *testing.T) { + project := types.Project{ + Services: types.Services{ + "myservice": { + Name: "myservice", + Image: "scratch", + Develop: &types.DevelopConfig{ + Watch: []types.Trigger{ + { + Action: tt.action, + Path: []string{"/app", "/app2"}, // should only be one path + Target: "/container/app", + }, + }, + }, + }, + }, + } + err := checkConsistency(&project) + assert.Error(t, err, "services.myservice.develop.watch: can only use more than one path for actions rebuild and restart: invalid compose project") + }) + } + tests = []WatchActionTest{ + {action: types.WatchActionRebuild}, + {action: types.WatchActionRestart}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("watch config is VALID with missing target for %s action", tt.action), func(t *testing.T) { + project := types.Project{ + Services: types.Services{ + "myservice": { + Name: "myservice", + Image: "scratch", + Develop: &types.DevelopConfig{ + Watch: []types.Trigger{ + { + Action: tt.action, + Path: []string{"/app"}, + }, + }, + }, + }, + }, + } + err := checkConsistency(&project) + assert.NilError(t, err) + }) + + t.Run(fmt.Sprintf("watch config is VALID with one or more paths for %s action", tt.action), func(t *testing.T) { + project := types.Project{ + Services: types.Services{ + "myservice": { + Name: "myservice", + Image: "scratch", + Develop: &types.DevelopConfig{ + Watch: []types.Trigger{ + { + Action: tt.action, + Path: []string{"/app"}, + }, + { + Action: tt.action, + Path: []string{"/app", "/app2"}, + }, + }, + }, + }, + }, + } + err := checkConsistency(&project) + assert.NilError(t, err) + }) + } } diff --git a/schema/compose-spec.json b/schema/compose-spec.json index 2faa3127..4bc0fb28 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -492,7 +492,7 @@ "required": ["path", "action"], "properties": { "ignore": {"type": "array", "items": {"type": "string"}}, - "path": {"type": "string"}, + "path": {"$ref": "#/definitions/string_or_list"}, "action": {"type": "string", "enum": ["rebuild", "sync", "restart", "sync+restart", "sync+exec"]}, "target": {"type": "string"}, "exec": {"$ref": "#/definitions/service_hook"} diff --git a/types/develop.go b/types/develop.go index 8f7c8fa5..ead272e2 100644 --- a/types/develop.go +++ b/types/develop.go @@ -33,7 +33,7 @@ const ( ) type Trigger struct { - Path string `yaml:"path" json:"path"` + Path StringList `yaml:"path" json:"path"` Action WatchAction `yaml:"action" json:"action"` Target string `yaml:"target,omitempty" json:"target,omitempty"` Exec ServiceHook `yaml:"exec,omitempty" json:"exec,omitempty"` diff --git a/validation/validation.go b/validation/validation.go index 793c1930..b71934b4 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -89,9 +89,18 @@ func checkFileObject(keys ...string) checkerFunc { } func checkPath(value any, p tree.Path) error { - v := value.(string) - if v == "" { - return fmt.Errorf("%s: value can't be blank", p) + switch v := value.(type) { + case string: + if v == "" { + return fmt.Errorf("%s: value can't be blank", p) + } + case []interface{}: + for _, el := range v { + e := el.(string) + if e == "" { + return fmt.Errorf("%s: value in paths can't be blank", e) + } + } } return nil } From a66c385b3358ec20341d3b394ced57651d78a781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joana=20Hrotk=C3=B3?= Date: Fri, 14 Feb 2025 15:18:37 +0000 Subject: [PATCH 2/3] Handle * when resolving paths in watch path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joana Hrotkó --- loader/loader_test.go | 125 +++++++++++------- .../watch/compose-test-watch-star.yaml | 11 ++ loader/testdata/watch/other.txt | 0 loader/testdata/watch/some-text.txt | 0 loader/validate.go | 2 +- loader/validate_test.go | 2 +- paths/unix.go | 41 +++++- transform/canonical.go | 1 + 8 files changed, 131 insertions(+), 51 deletions(-) create mode 100644 loader/testdata/watch/compose-test-watch-star.yaml create mode 100644 loader/testdata/watch/other.txt create mode 100644 loader/testdata/watch/some-text.txt diff --git a/loader/loader_test.go b/loader/loader_test.go index 2a4a8de8..3b567c6d 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -3051,7 +3051,8 @@ services: } func TestLoadDevelopConfig(t *testing.T) { - project, err := LoadWithContext(context.TODO(), buildConfigDetails(` + t.Run("successfully load watch config", func(t *testing.T) { + project, err := LoadWithContext(context.Background(), buildConfigDetails(` name: load-develop services: frontend: @@ -3066,7 +3067,6 @@ services: target: /var/www ignore: - node_modules/ - backend: image: example/backend build: ./backend @@ -3077,7 +3077,6 @@ services: path: - ./backend/src - ./backend - proxy: image: example/proxy build: ./proxy @@ -3088,50 +3087,50 @@ services: action: sync+restart target: /etc/nginx/proxy.conf `, nil), func(options *Options) { - options.ResolvePaths = false - options.SkipValidation = true - }) - assert.NilError(t, err) - frontend, err := project.GetService("frontend") - assert.NilError(t, err) - assert.DeepEqual(t, *frontend.Develop, types.DevelopConfig{ - Watch: []types.Trigger{ - { - Path: []string{"./webapp/html"}, - Action: types.WatchActionSync, - Target: "/var/www", - Ignore: []string{"node_modules/"}, - Extensions: types.Extensions{ - "x-initialSync": true, + options.ResolvePaths = false + options.SkipValidation = true + }) + assert.NilError(t, err) + frontend, err := project.GetService("frontend") + assert.NilError(t, err) + assert.DeepEqual(t, *frontend.Develop, types.DevelopConfig{ + Watch: []types.Trigger{ + { + Path: []string{"./webapp/html"}, + Action: types.WatchActionSync, + Target: "/var/www", + Ignore: []string{"node_modules/"}, + Extensions: types.Extensions{ + "x-initialSync": true, + }, }, }, - }, - }) - backend, err := project.GetService("backend") - assert.NilError(t, err) - assert.DeepEqual(t, *backend.Develop, types.DevelopConfig{ - Watch: []types.Trigger{ - { - Path: []string{"./backend/src", "./backend"}, - Action: types.WatchActionRebuild, + }) + backend, err := project.GetService("backend") + assert.NilError(t, err) + assert.DeepEqual(t, *backend.Develop, types.DevelopConfig{ + Watch: []types.Trigger{ + { + Path: []string{"./backend/src", "./backend"}, + Action: types.WatchActionRebuild, + }, }, - }, - }) - proxy, err := project.GetService("proxy") - assert.NilError(t, err) - assert.DeepEqual(t, *proxy.Develop, types.DevelopConfig{ - Watch: []types.Trigger{ - { - Path: []string{"./proxy/proxy.conf"}, - Action: types.WatchActionSyncRestart, - Target: "/etc/nginx/proxy.conf", + }) + proxy, err := project.GetService("proxy") + assert.NilError(t, err) + assert.DeepEqual(t, *proxy.Develop, types.DevelopConfig{ + Watch: []types.Trigger{ + { + Path: []string{"./proxy/proxy.conf"}, + Action: types.WatchActionSyncRestart, + Target: "/etc/nginx/proxy.conf", + }, }, - }, + }) }) -} -func TestBadDevelopConfig(t *testing.T) { - _, err := LoadWithContext(context.TODO(), buildConfigDetails(` + t.Run("should not load successfully bad watch config", func(t *testing.T) { + _, err := LoadWithContext(context.TODO(), buildConfigDetails(` name: load-develop services: frontend: @@ -3139,16 +3138,52 @@ services: build: ./webapp develop: watch: - # sync static content + # sync static content - path: ./webapp/html target: /var/www ignore: - node_modules/ - `, nil), func(options *Options) { - options.ResolvePaths = false + options.ResolvePaths = false + }) + assert.ErrorContains(t, err, "validating filename0.yml: services.frontend.develop.watch.0 action is required") + }) + + t.Run("should return an error when cannot resolve path", func(t *testing.T) { + b, err := os.ReadFile("testdata/watch/compose-test-watch-star.yaml") + assert.NilError(t, err) + + configDetails := types.ConfigDetails{ + WorkingDir: "testdata", + ConfigFiles: []types.ConfigFile{ + {Filename: "watch/compose-test-watch-star.yaml", Content: b}, + }, + Environment: map[string]string{}, + } + expServices := types.Services{ + "app": { + Name: "app", + Image: "example/app", + Environment: types.MappingWithEquals{}, + Networks: map[string]*types.ServiceNetworkConfig{"default": nil}, + Develop: &types.DevelopConfig{ + Watch: []types.Trigger{ + { + Path: []string{ + filepath.FromSlash("testdata/watch/other.txt"), + filepath.FromSlash("testdata/watch/some-text.txt"), + }, + Action: types.WatchActionRebuild, + }, + }, + }, + }, + } + + actual, err := LoadWithContext(context.Background(), configDetails) + assert.NilError(t, err) + assert.DeepEqual(t, actual.Services, expServices) }) - assert.ErrorContains(t, err, "validating filename0.yml: services.frontend.develop.watch.0 action is required") } func TestBadServiceConfig(t *testing.T) { diff --git a/loader/testdata/watch/compose-test-watch-star.yaml b/loader/testdata/watch/compose-test-watch-star.yaml new file mode 100644 index 00000000..7cfcc3cb --- /dev/null +++ b/loader/testdata/watch/compose-test-watch-star.yaml @@ -0,0 +1,11 @@ +name: compose-test-watch-star +services: + app: + image: example/app + develop: + watch: + - path: ./watch/*.txt + action: rebuild + # - path: ./watch/* + # target: ./app + # action: sync diff --git a/loader/testdata/watch/other.txt b/loader/testdata/watch/other.txt new file mode 100644 index 00000000..e69de29b diff --git a/loader/testdata/watch/some-text.txt b/loader/testdata/watch/some-text.txt new file mode 100644 index 00000000..e69de29b diff --git a/loader/validate.go b/loader/validate.go index 943447c3..8babb8a1 100644 --- a/loader/validate.go +++ b/loader/validate.go @@ -173,7 +173,7 @@ func checkConsistency(project *types.Project) error { //nolint:gocyclo } if len(watch.Path) > 1 { - return fmt.Errorf("services.%s.develop.watch: can only use more than one path for actions %s and %s: %w", s.Name, types.WatchActionRebuild, types.WatchActionRestart, errdefs.ErrInvalid) + return fmt.Errorf("services.%s.develop.watch: detected multiple paths %s for action %s. Multiple files are only valid for %s and %s actions: %w", s.Name, watch.Path, watch.Action, types.WatchActionRebuild, types.WatchActionRestart, errdefs.ErrInvalid) } } } diff --git a/loader/validate_test.go b/loader/validate_test.go index 9c08a0ba..b75fefbf 100644 --- a/loader/validate_test.go +++ b/loader/validate_test.go @@ -395,7 +395,7 @@ func TestValidateWatch(t *testing.T) { }, } err := checkConsistency(&project) - assert.Error(t, err, "services.myservice.develop.watch: can only use more than one path for actions rebuild and restart: invalid compose project") + assert.ErrorContains(t, err, "services.myservice.develop.watch: detected multiple paths") }) } tests = []WatchActionTest{ diff --git a/paths/unix.go b/paths/unix.go index d47f9366..54d4c756 100644 --- a/paths/unix.go +++ b/paths/unix.go @@ -17,6 +17,7 @@ package paths import ( + "fmt" "path" "path/filepath" @@ -49,9 +50,41 @@ func (r *relativePathsResolver) absSymbolicLink(value any) (any, error) { if err != nil { return nil, err } - str, ok := abs.(string) - if !ok { - return abs, nil + switch t := abs.(type) { + case string: + // this can return []string if * matches more than one file + return resolveAbsStarPath(t) + case []any: + var res []any + for _, tt := range t { + s, _ := tt.(string) + r, err := resolveAbsStarPath(s) + if err != nil { + return nil, err + } + res = append(res, r...) + } + return res, nil + } + + return abs, nil +} + +func resolveAbsStarPath(t string) ([]any, error) { + matches, err := filepath.Glob(t) + if err != nil { + return nil, err + } + if len(matches) == 0 { + return nil, fmt.Errorf("could not resolve %s. Please make sure it exists?", t) + } + res := make([]any, len(matches)) + for i, m := range matches { + symb, err := utils.ResolveSymbolicLink(m) + if err != nil { + return nil, err + } + res[i] = symb } - return utils.ResolveSymbolicLink(str) + return res, nil } diff --git a/transform/canonical.go b/transform/canonical.go index d37eb1e2..d60104e0 100644 --- a/transform/canonical.go +++ b/transform/canonical.go @@ -36,6 +36,7 @@ func init() { transformers["services.*.networks"] = transformServiceNetworks transformers["services.*.volumes.*"] = transformVolumeMount transformers["services.*.dns"] = transformStringOrList + transformers["services.*.develop.watch.*.path"] = transformStringOrList transformers["services.*.devices.*"] = transformDeviceMapping transformers["services.*.secrets.*"] = transformFileMount transformers["services.*.configs.*"] = transformFileMount From 07d331d3260affede1109354888e3d8bb83390ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joana=20Hrotk=C3=B3?= Date: Tue, 25 Feb 2025 15:34:15 +0000 Subject: [PATCH 3/3] Revert Path to string and convert path list to watch config list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joana Hrotkó --- loader/loader_test.go | 46 ++++++--------------------------------- loader/validate.go | 7 ++---- loader/validate_test.go | 32 +++++---------------------- paths/unix.go | 41 ++++------------------------------- transform/canonical.go | 47 +++++++++++++++++++++++++++++++++++++++- types/develop.go | 31 +++++++++++++++++++++++++- validation/validation.go | 16 ++++---------- 7 files changed, 98 insertions(+), 122 deletions(-) diff --git a/loader/loader_test.go b/loader/loader_test.go index 3b567c6d..6dee4943 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -3096,7 +3096,7 @@ services: assert.DeepEqual(t, *frontend.Develop, types.DevelopConfig{ Watch: []types.Trigger{ { - Path: []string{"./webapp/html"}, + Path: "./webapp/html", Action: types.WatchActionSync, Target: "/var/www", Ignore: []string{"node_modules/"}, @@ -3111,7 +3111,11 @@ services: assert.DeepEqual(t, *backend.Develop, types.DevelopConfig{ Watch: []types.Trigger{ { - Path: []string{"./backend/src", "./backend"}, + Path: "./backend/src", + Action: types.WatchActionRebuild, + }, + { + Path: "./backend", Action: types.WatchActionRebuild, }, }, @@ -3121,7 +3125,7 @@ services: assert.DeepEqual(t, *proxy.Develop, types.DevelopConfig{ Watch: []types.Trigger{ { - Path: []string{"./proxy/proxy.conf"}, + Path: "./proxy/proxy.conf", Action: types.WatchActionSyncRestart, Target: "/etc/nginx/proxy.conf", }, @@ -3148,42 +3152,6 @@ services: }) assert.ErrorContains(t, err, "validating filename0.yml: services.frontend.develop.watch.0 action is required") }) - - t.Run("should return an error when cannot resolve path", func(t *testing.T) { - b, err := os.ReadFile("testdata/watch/compose-test-watch-star.yaml") - assert.NilError(t, err) - - configDetails := types.ConfigDetails{ - WorkingDir: "testdata", - ConfigFiles: []types.ConfigFile{ - {Filename: "watch/compose-test-watch-star.yaml", Content: b}, - }, - Environment: map[string]string{}, - } - expServices := types.Services{ - "app": { - Name: "app", - Image: "example/app", - Environment: types.MappingWithEquals{}, - Networks: map[string]*types.ServiceNetworkConfig{"default": nil}, - Develop: &types.DevelopConfig{ - Watch: []types.Trigger{ - { - Path: []string{ - filepath.FromSlash("testdata/watch/other.txt"), - filepath.FromSlash("testdata/watch/some-text.txt"), - }, - Action: types.WatchActionRebuild, - }, - }, - }, - }, - } - - actual, err := LoadWithContext(context.Background(), configDetails) - assert.NilError(t, err) - assert.DeepEqual(t, actual.Services, expServices) - }) } func TestBadServiceConfig(t *testing.T) { diff --git a/loader/validate.go b/loader/validate.go index 8babb8a1..b528cb8c 100644 --- a/loader/validate.go +++ b/loader/validate.go @@ -169,11 +169,8 @@ func checkConsistency(project *types.Project) error { //nolint:gocyclo for _, watch := range s.Develop.Watch { if watch.Action != types.WatchActionRebuild && watch.Action != types.WatchActionRestart { if watch.Target == "" { - return fmt.Errorf("services.%s.develop.watch: target is required for %s, %s and %s actions: %w", s.Name, types.WatchActionSync, types.WatchActionSyncExec, types.WatchActionSyncRestart, errdefs.ErrInvalid) - - } - if len(watch.Path) > 1 { - return fmt.Errorf("services.%s.develop.watch: detected multiple paths %s for action %s. Multiple files are only valid for %s and %s actions: %w", s.Name, watch.Path, watch.Action, types.WatchActionRebuild, types.WatchActionRestart, errdefs.ErrInvalid) + return fmt.Errorf("services.%s.develop.watch: target is required for %s, %s and %s actions: %w", + s.Name, types.WatchActionSync, types.WatchActionSyncExec, types.WatchActionSyncRestart, errdefs.ErrInvalid) } } } diff --git a/loader/validate_test.go b/loader/validate_test.go index b75fefbf..020a8fdd 100644 --- a/loader/validate_test.go +++ b/loader/validate_test.go @@ -292,7 +292,7 @@ func TestValidateWatch(t *testing.T) { Watch: []types.Trigger{ { Action: types.WatchActionSync, - Path: []string{"/app"}, + Path: "/app", Target: "/container/app", }, }, @@ -364,7 +364,7 @@ func TestValidateWatch(t *testing.T) { Watch: []types.Trigger{ { Action: tt.action, - Path: []string{"/app"}, + Path: "/app", // Missing Target }, }, @@ -375,28 +375,6 @@ func TestValidateWatch(t *testing.T) { err := checkConsistency(&project) assert.Error(t, err, "services.myservice.develop.watch: target is required for sync, sync+exec and sync+restart actions: invalid compose project") }) - - t.Run(fmt.Sprintf("watch config is INVALID with one or more paths for %s action", tt.action), func(t *testing.T) { - project := types.Project{ - Services: types.Services{ - "myservice": { - Name: "myservice", - Image: "scratch", - Develop: &types.DevelopConfig{ - Watch: []types.Trigger{ - { - Action: tt.action, - Path: []string{"/app", "/app2"}, // should only be one path - Target: "/container/app", - }, - }, - }, - }, - }, - } - err := checkConsistency(&project) - assert.ErrorContains(t, err, "services.myservice.develop.watch: detected multiple paths") - }) } tests = []WatchActionTest{ {action: types.WatchActionRebuild}, @@ -413,7 +391,7 @@ func TestValidateWatch(t *testing.T) { Watch: []types.Trigger{ { Action: tt.action, - Path: []string{"/app"}, + Path: "/app", }, }, }, @@ -434,11 +412,11 @@ func TestValidateWatch(t *testing.T) { Watch: []types.Trigger{ { Action: tt.action, - Path: []string{"/app"}, + Path: "/app", }, { Action: tt.action, - Path: []string{"/app", "/app2"}, + Path: "/app2", }, }, }, diff --git a/paths/unix.go b/paths/unix.go index 54d4c756..d47f9366 100644 --- a/paths/unix.go +++ b/paths/unix.go @@ -17,7 +17,6 @@ package paths import ( - "fmt" "path" "path/filepath" @@ -50,41 +49,9 @@ func (r *relativePathsResolver) absSymbolicLink(value any) (any, error) { if err != nil { return nil, err } - switch t := abs.(type) { - case string: - // this can return []string if * matches more than one file - return resolveAbsStarPath(t) - case []any: - var res []any - for _, tt := range t { - s, _ := tt.(string) - r, err := resolveAbsStarPath(s) - if err != nil { - return nil, err - } - res = append(res, r...) - } - return res, nil - } - - return abs, nil -} - -func resolveAbsStarPath(t string) ([]any, error) { - matches, err := filepath.Glob(t) - if err != nil { - return nil, err - } - if len(matches) == 0 { - return nil, fmt.Errorf("could not resolve %s. Please make sure it exists?", t) - } - res := make([]any, len(matches)) - for i, m := range matches { - symb, err := utils.ResolveSymbolicLink(m) - if err != nil { - return nil, err - } - res[i] = symb + str, ok := abs.(string) + if !ok { + return abs, nil } - return res, nil + return utils.ResolveSymbolicLink(str) } diff --git a/transform/canonical.go b/transform/canonical.go index d60104e0..abb62698 100644 --- a/transform/canonical.go +++ b/transform/canonical.go @@ -36,7 +36,7 @@ func init() { transformers["services.*.networks"] = transformServiceNetworks transformers["services.*.volumes.*"] = transformVolumeMount transformers["services.*.dns"] = transformStringOrList - transformers["services.*.develop.watch.*.path"] = transformStringOrList + transformers["services.*.develop.watch"] = transformWatch transformers["services.*.devices.*"] = transformDeviceMapping transformers["services.*.secrets.*"] = transformFileMount transformers["services.*.configs.*"] = transformFileMount @@ -52,6 +52,51 @@ func init() { transformers["include.*"] = transformInclude } +func transformWatch(data any, _ tree.Path, _ bool) (any, error) { + t, ok := data.([]interface{}) + if !ok { + return data, nil + } + + for i, w := range t { + watchConf, ok := w.(map[string]interface{}) + if !ok { + continue + } + path, ok := watchConf["path"] + if !ok { + // This should not happen + continue + } + paths, ok := path.([]interface{}) + if !ok { + // if path is a string there is nothing to do + continue + } + + // remove the current path that is a list + if i == len(t)-1 { + t = t[:i] + } else { + t = append(t[:i], t[i+1:]) + } + + // transform each element into a watch item + for _, p := range paths { + extend := make(map[string]interface{}) + for k, v := range watchConf { + if k == "path" { + extend[k] = p + continue + } + extend[k] = v + } + t = append(t, extend) + } + } + return t, nil +} + func transformStringOrList(data any, _ tree.Path, _ bool) (any, error) { switch t := data.(type) { case string: diff --git a/types/develop.go b/types/develop.go index ead272e2..51025c85 100644 --- a/types/develop.go +++ b/types/develop.go @@ -16,6 +16,11 @@ package types +import ( + "path/filepath" + "strings" +) + type DevelopConfig struct { Watch []Trigger `yaml:"watch,omitempty" json:"watch,omitempty"` @@ -33,10 +38,34 @@ const ( ) type Trigger struct { - Path StringList `yaml:"path" json:"path"` + Path string `yaml:"path" json:"path"` Action WatchAction `yaml:"action" json:"action"` Target string `yaml:"target,omitempty" json:"target,omitempty"` Exec ServiceHook `yaml:"exec,omitempty" json:"exec,omitempty"` Ignore []string `yaml:"ignore,omitempty" json:"ignore,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } + +func (t Trigger) AnchorPath() string { + if t.IsGlobPath() { + pathList := strings.Split(filepath.FromSlash(t.Path), "/") + path := []string{} + + for _, a := range pathList { + if strings.Contains(a, "*") { + break + } + path = append(path, a) + } + return strings.Join(path, string(filepath.Separator)) + } + return t.Path +} + +func (t Trigger) IsGlobPath() bool { + return strings.Contains(t.Path, "*") +} + +func (t Trigger) IsSyncAction() bool { + return t.Action == WatchActionSync || t.Action == WatchActionSyncRestart +} diff --git a/validation/validation.go b/validation/validation.go index b71934b4..613919b2 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -89,19 +89,11 @@ func checkFileObject(keys ...string) checkerFunc { } func checkPath(value any, p tree.Path) error { - switch v := value.(type) { - case string: - if v == "" { - return fmt.Errorf("%s: value can't be blank", p) - } - case []interface{}: - for _, el := range v { - e := el.(string) - if e == "" { - return fmt.Errorf("%s: value in paths can't be blank", e) - } - } + v := value.(string) + if v == "" { + return fmt.Errorf("%s: value can't be blank", p) } + return nil }