From 4aacd0225d2007e6f7199fe959398d851cb89152 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] 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 | 11 ++- loader/validate_test.go | 171 ++++++++++++++++++++++++--------------- schema/compose-spec.json | 2 +- types/develop.go | 2 +- validation/validation.go | 15 +++- 6 files changed, 137 insertions(+), 77 deletions(-) diff --git a/loader/loader_test.go b/loader/loader_test.go index 234107f4..ad9f7fbf 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -3119,8 +3119,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 @@ -3140,7 +3143,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/"}, @@ -3155,7 +3158,7 @@ services: assert.DeepEqual(t, *backend.Develop, types.DevelopConfig{ Watch: []types.Trigger{ { - Path: "./backend/src", + Path: []string{"./backend/src", "./backend"}, Action: types.WatchActionRebuild, }, }, @@ -3165,7 +3168,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 0feb2a96..c7b69d3f 100644 --- a/loader/validate.go +++ b/loader/validate.go @@ -167,11 +167,16 @@ func checkConsistency(project *types.Project) error { 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 0575a02e..34d03be5 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", }, }, @@ -304,69 +305,6 @@ func TestValidateWatch(t *testing.T) { }) - 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{ @@ -407,4 +345,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 1da7f228..55ec4847 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -491,7 +491,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 707f247e..6385ed1a 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -90,9 +90,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 }