Skip to content

Commit 2905bf8

Browse files
authored
Improve lakectl actions valdiate by checking required properties (#10144)
* Validate action hook properties at parse time Add early validation of hook properties during action parsing so configuration errors are caught at parse time instead of runtime. Register ValidatePropertiesFunc validators for webhook (requires url), airflow (requires url, dag_id, username, password), and lua (requires script or script_path) hooks. * Actions: remove hookType param from requireProperties Move hook type context from requireProperties() into the caller's error wrapping in Validate(), following Go's idiomatic error chaining.
1 parent ca952f0 commit 2905bf8

10 files changed

Lines changed: 108 additions & 8 deletions

pkg/actions/action.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ func (a *Action) Validate() error {
9696
if _, found := hooks[hook.Type]; !found {
9797
return fmt.Errorf("hook[%d] type '%s' unknown: %w", i, hook.ID, ErrInvalidAction)
9898
}
99+
if validate, found := hookValidators[hook.Type]; found {
100+
if err := validate(hook.Properties); err != nil {
101+
return fmt.Errorf("hook[%d] type '%s': %w", i, hook.Type, err)
102+
}
103+
}
99104
}
100105
return nil
101106
}

pkg/actions/action_test.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ func TestAction_ReadAction(t *testing.T) {
3131
{name: "invalid event type", filename: "action_invalid_event.yaml", errStr: "event 'not-a-valid-event' is not supported: invalid action"},
3232
{name: "invalid yaml", filename: "action_invalid_yaml.yaml", errStr: "yaml: unmarshal errors"},
3333
{name: "invalid parameter in tag event", filename: "action_invalid_param_tag_actions.yaml", errStr: "'branches' is not supported in tag event types"},
34+
{name: "lua misplaced script", filename: "action_lua_missing_script.yaml", errStr: "missing hook properties"},
35+
{name: "lua empty properties", filename: "action_lua_empty_properties.yaml", errStr: "'script' or 'script_path' must be supplied in properties"},
36+
{name: "lua no properties", filename: "action_lua_no_properties.yaml", errStr: "missing hook properties"},
37+
{name: "lua valid script", filename: "action_lua_valid_script.yaml"},
38+
{name: "lua valid script_path", filename: "action_lua_valid_script_path.yaml"},
39+
{name: "webhook missing url", filename: "action_webhook_missing_url.yaml", errStr: "'url' must be supplied in properties"},
40+
{name: "airflow missing props", filename: "action_airflow_missing_props.yaml", errStr: "'dag_id' must be supplied in properties"},
3441
}
3542
for _, tt := range tests {
3643
t.Run(tt.name, func(t *testing.T) {
@@ -302,8 +309,9 @@ func TestLoadActions(t *testing.T) {
302309
},
303310
Hooks: []actions.ActionHook{
304311
{
305-
ID: "hook_id",
306-
Type: "webhook",
312+
ID: "hook_id",
313+
Type: "webhook",
314+
Properties: map[string]any{"url": "https://example.com/hook"},
307315
},
308316
},
309317
}))
@@ -326,12 +334,14 @@ func TestLoadActions(t *testing.T) {
326334
},
327335
Hooks: []actions.ActionHook{
328336
{
329-
ID: "hook_id_1",
330-
Type: "webhook",
337+
ID: "hook_id_1",
338+
Type: "webhook",
339+
Properties: map[string]any{"url": "https://example.com/hook1"},
331340
},
332341
{
333-
ID: "hook_id_2",
334-
Type: "webhook",
342+
ID: "hook_id_2",
343+
Type: "webhook",
344+
Properties: map[string]any{"url": "https://example.com/hook2"},
335345
},
336346
},
337347
}))
@@ -347,12 +357,12 @@ func TestLoadActions(t *testing.T) {
347357
{
348358
ID: "hook_id_1",
349359
Type: "webhook",
350-
Properties: map[string]any{},
360+
Properties: map[string]any{"url": "https://example.com/hook1"},
351361
},
352362
{
353363
ID: "hook_id_2",
354364
Type: "webhook",
355-
Properties: map[string]any{},
365+
Properties: map[string]any{"url": "https://example.com/hook2"},
356366
},
357367
},
358368
},

pkg/actions/hook.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"context"
66
"fmt"
77
"net/http"
8+
"slices"
9+
"strings"
810

911
"github.com/treeverse/lakefs/pkg/graveler"
1012
"github.com/treeverse/lakefs/pkg/stats"
@@ -32,12 +34,40 @@ type HookBase struct {
3234
Endpoint *http.Server
3335
}
3436

37+
type ValidatePropertiesFunc func(Properties) error
38+
39+
// requireProperties returns a ValidatePropertiesFunc that checks required property groups.
40+
// Each group is a slice of keys where at least one must be present (OR within a group).
41+
// All groups must be satisfied (AND between groups).
42+
func requireProperties(groups ...[]string) ValidatePropertiesFunc {
43+
return func(p Properties) error {
44+
if p == nil {
45+
return fmt.Errorf("missing hook properties: %w", ErrInvalidAction)
46+
}
47+
for _, group := range groups {
48+
if !slices.ContainsFunc(group, func(key string) bool {
49+
_, has := p[key]
50+
return has
51+
}) {
52+
return fmt.Errorf("'%s' must be supplied in properties: %w", strings.Join(group, "' or '"), ErrInvalidAction)
53+
}
54+
}
55+
return nil
56+
}
57+
}
58+
3559
var hooks = map[HookType]NewHookFunc{
3660
HookTypeWebhook: NewWebhook,
3761
HookTypeAirflow: NewAirflowHook,
3862
HookTypeLua: NewLuaHook,
3963
}
4064

65+
var hookValidators = map[HookType]ValidatePropertiesFunc{
66+
HookTypeWebhook: requireProperties([]string{"url"}),
67+
HookTypeAirflow: requireProperties([]string{"url"}, []string{"dag_id"}, []string{"username"}, []string{"password"}),
68+
HookTypeLua: requireProperties([]string{"script", "script_path"}),
69+
}
70+
4171
func NewHook(hook ActionHook, action *Action, cfg Config, server *http.Server, serverAddress string, collector stats.Collector) (Hook, error) {
4272
f := hooks[hook.Type]
4373
if f == nil {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: Airflow missing props
2+
on:
3+
pre-commit:
4+
hooks:
5+
- id: my_airflow
6+
type: airflow
7+
properties:
8+
url: "https://airflow.example.com"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name: Lua empty properties
2+
on:
3+
pre-commit:
4+
hooks:
5+
- id: check_metadata
6+
type: lua
7+
properties: {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: Lua missing script
2+
on:
3+
pre-commit:
4+
hooks:
5+
- id: check_metadata
6+
type: lua
7+
properties:
8+
script: |
9+
print("this is at the wrong indentation level")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: Lua no properties
2+
on:
3+
pre-commit:
4+
hooks:
5+
- id: check_metadata
6+
type: lua
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: Lua valid script
2+
on:
3+
pre-commit:
4+
hooks:
5+
- id: check_metadata
6+
type: lua
7+
properties:
8+
script: |
9+
print("hello")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: Lua valid script path
2+
on:
3+
pre-commit:
4+
hooks:
5+
- id: check_metadata
6+
type: lua
7+
properties:
8+
script_path: scripts/check.lua
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: Webhook missing url
2+
on:
3+
pre-commit:
4+
hooks:
5+
- id: my_webhook
6+
type: webhook
7+
properties:
8+
timeout: 30s

0 commit comments

Comments
 (0)