Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 08c99b7

Browse files
committed
Add the validator
Signed-off-by: Djordje Lukic <[email protected]>
1 parent e1d4364 commit 08c99b7

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

internal/validator/validator.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package validator
2+
3+
import (
4+
"io/ioutil"
5+
"sort"
6+
"strings"
7+
8+
"github.com/docker/app/internal/validator/rules"
9+
composeloader "github.com/docker/cli/cli/compose/loader"
10+
"github.com/pkg/errors"
11+
)
12+
13+
type Validator struct {
14+
Rules []rules.Rule
15+
errors []error
16+
}
17+
18+
type ValidationError struct {
19+
Errors []error
20+
}
21+
22+
type ValidationCallback func(string, string, interface{})
23+
24+
func (v ValidationError) Error() string {
25+
parts := []string{}
26+
for _, err := range v.Errors {
27+
parts = append(parts, "* "+err.Error())
28+
}
29+
30+
sort.Strings(parts)
31+
parts = append([]string{"Compose file validation failed:"}, parts...)
32+
33+
return strings.Join(parts, "\n")
34+
}
35+
36+
type Config func(*Validator)
37+
type Opt func(c *Validator) error
38+
39+
func NewValidator(opts ...Config) Validator {
40+
validator := Validator{}
41+
for _, opt := range opts {
42+
opt(&validator)
43+
}
44+
return validator
45+
}
46+
47+
func WithRelativePathRule() Config {
48+
return func(v *Validator) {
49+
v.Rules = append(v.Rules, rules.NewRelativePathRule())
50+
}
51+
}
52+
53+
func WithExternalSecretsRule() Config {
54+
return func(v *Validator) {
55+
v.Rules = append(v.Rules, rules.NewExternalSecretsRule())
56+
}
57+
}
58+
59+
func NewValidatorWithDefaults() Validator {
60+
return NewValidator(
61+
WithRelativePathRule(),
62+
WithExternalSecretsRule(),
63+
)
64+
}
65+
66+
// Validate validates the compose file, it returns an error
67+
// if it can't parse the compose file or a ValidationError
68+
// that contains all the validation errors (if any), nil otherwise
69+
func (v *Validator) Validate(composeFile string) error {
70+
composeRaw, err := ioutil.ReadFile(composeFile)
71+
if err != nil {
72+
return errors.Wrapf(err, "failed to read compose file %q", composeFile)
73+
}
74+
cfgMap, err := composeloader.ParseYAML(composeRaw)
75+
if err != nil {
76+
return errors.Wrap(err, "failed to parse compose file")
77+
}
78+
79+
// First phase, the rules collect all the dependent values they need
80+
v.visitAll("", cfgMap, v.collect)
81+
// Second phase, validate the compose file
82+
v.visitAll("", cfgMap, v.validate)
83+
84+
if len(v.errors) > 0 {
85+
return ValidationError{
86+
Errors: v.errors,
87+
}
88+
}
89+
return nil
90+
}
91+
92+
func (v *Validator) collect(parent string, key string, value interface{}) {
93+
for _, rule := range v.Rules {
94+
rule.Collect(parent, key, value)
95+
}
96+
}
97+
98+
func (v *Validator) validate(parent string, key string, value interface{}) {
99+
for _, rule := range v.Rules {
100+
if rule.Accept(parent, key) {
101+
verrs := rule.Validate(value)
102+
if len(verrs) > 0 {
103+
v.errors = append(v.errors, verrs...)
104+
}
105+
}
106+
}
107+
}
108+
109+
func (v *Validator) visitAll(parent string, cfgMap interface{}, cb ValidationCallback) {
110+
m, ok := cfgMap.(map[string]interface{})
111+
if !ok {
112+
return
113+
}
114+
115+
for key, value := range m {
116+
switch value := value.(type) {
117+
case string:
118+
continue
119+
default:
120+
cb(parent, key, value)
121+
122+
path := parent + "." + key
123+
if parent == "" {
124+
path = key
125+
}
126+
127+
sub, ok := m[key].(map[string]interface{})
128+
if ok {
129+
v.visitAll(path, sub, cb)
130+
}
131+
}
132+
}
133+
}

internal/validator/validator_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package validator
2+
3+
import (
4+
"testing"
5+
6+
"github.com/docker/app/internal"
7+
"gotest.tools/assert"
8+
"gotest.tools/fs"
9+
)
10+
11+
type mockRule struct {
12+
acceptCalled bool
13+
validateCalled bool
14+
}
15+
16+
func (m *mockRule) Collect(path string, key string, value interface{}) {
17+
18+
}
19+
20+
func (m *mockRule) Accept(path string, key string) bool {
21+
m.acceptCalled = true
22+
return true
23+
}
24+
25+
func (m *mockRule) Validate(value interface{}) []error {
26+
m.validateCalled = true
27+
return nil
28+
}
29+
30+
func TestValidate(t *testing.T) {
31+
composeData := `
32+
version: '3.7'
33+
services:
34+
nginx:
35+
image: nginx
36+
volumes:
37+
- ./foo:/data
38+
`
39+
inputDir := fs.NewDir(t, "app_input_",
40+
fs.WithFile(internal.ComposeFileName, composeData),
41+
)
42+
defer inputDir.Remove()
43+
44+
appName := "my.dockerapp"
45+
dir := fs.NewDir(t, "app_",
46+
fs.WithDir(appName),
47+
)
48+
defer dir.Remove()
49+
50+
r := &mockRule{}
51+
v := NewValidator(func(v *Validator) {
52+
v.Rules = append(v.Rules, r)
53+
})
54+
55+
err := v.Validate(inputDir.Join(internal.ComposeFileName))
56+
assert.NilError(t, err)
57+
assert.Equal(t, r.acceptCalled, true)
58+
assert.Equal(t, r.validateCalled, true)
59+
}

0 commit comments

Comments
 (0)