Skip to content

Commit

Permalink
Add multiple paths for rebuild and restart
Browse files Browse the repository at this point in the history
Signed-off-by: Joana Hrotkó <[email protected]>
  • Loading branch information
jhrotko committed Feb 13, 2025
1 parent 84b7d5a commit 4aacd02
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 77 deletions.
13 changes: 8 additions & 5 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/"},
Expand All @@ -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,
},
},
Expand All @@ -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",
},
Expand Down
11 changes: 8 additions & 3 deletions loader/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

}
}

Expand Down
171 changes: 107 additions & 64 deletions loader/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package loader

import (
"fmt"
"strings"
"testing"

Expand Down Expand Up @@ -291,7 +292,7 @@ func TestValidateWatch(t *testing.T) {
Watch: []types.Trigger{
{
Action: types.WatchActionSync,
Path: "/app",
Path: []string{"/app"},
Target: "/container/app",
},
},
Expand All @@ -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{
Expand Down Expand Up @@ -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)
})
}
}
2 changes: 1 addition & 1 deletion schema/compose-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
2 changes: 1 addition & 1 deletion types/develop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
15 changes: 12 additions & 3 deletions validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 4aacd02

Please sign in to comment.