Skip to content
This repository was archived by the owner on Jun 16, 2021. It is now read-only.

Commit 20ad9f1

Browse files
committed
Strict yaml validation
Signed-off-by: Josh Curl <hello@joshcurl.com>
1 parent 613847e commit 20ad9f1

14 files changed

+1086
-7
lines changed

generate.go

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package libcompose
2+
3+
//go:generate go run script/inline_schema.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
base:
2+
image: busybox
3+
ports: invalid_type
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
base:
2+
image: busybox

integration/create_test.go

+88-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"os/exec"
7+
"strings"
78

89
. "gopkg.in/check.v1"
910
"path/filepath"
@@ -149,7 +150,6 @@ func (s *RunSuite) TestFieldTypeConversions(c *C) {
149150
image: tianon/true
150151
mem_limit: $LIMIT
151152
memswap_limit: "40000000"
152-
hostname: 100
153153
`)
154154

155155
name := fmt.Sprintf("%s_%s_1", p, "test")
@@ -160,7 +160,6 @@ func (s *RunSuite) TestFieldTypeConversions(c *C) {
160160
image: tianon/true
161161
mem_limit: 40000000
162162
memswap_limit: 40000000
163-
hostname: "100"
164163
`)
165164

166165
name = fmt.Sprintf("%s_%s_1", p, "reference")
@@ -257,5 +256,92 @@ func (s *RunSuite) TestDefaultMultipleComposeFiles(c *C) {
257256

258257
c.Assert(container, NotNil)
259258
}
259+
}
260+
261+
func (s *RunSuite) TestValidation(c *C) {
262+
template := `
263+
test:
264+
image: busybox
265+
ports: invalid_type
266+
`
267+
_, output := s.FromTextCaptureOutput(c, s.RandomProject(), "create", template)
268+
269+
c.Assert(strings.Contains(output, "Service 'test' configuration key 'ports' contains an invalid type, it should be an array."), Equals, true)
270+
271+
template = `
272+
test:
273+
image: busybox
274+
build: .
275+
`
276+
_, output = s.FromTextCaptureOutput(c, s.RandomProject(), "create", template)
277+
278+
c.Assert(strings.Contains(output, "Service 'test' has both an image and build path specified. A service can either be built to image or use an existing image, not both."), Equals, true)
279+
280+
template = `
281+
test:
282+
image: busybox
283+
ports: invalid_type
284+
links: invalid_type
285+
devices:
286+
- /dev/foo:/dev/foo
287+
- /dev/foo:/dev/foo
288+
`
289+
_, output = s.FromTextCaptureOutput(c, s.RandomProject(), "create", template)
290+
291+
c.Assert(strings.Contains(output, "Service 'test' configuration key 'ports' contains an invalid type, it should be an array."), Equals, true)
292+
c.Assert(strings.Contains(output, "Service 'test' configuration key 'links' contains an invalid type, it should be an array"), Equals, true)
293+
c.Assert(strings.Contains(output, "Service 'test' configuration key 'devices' value [/dev/foo:/dev/foo /dev/foo:/dev/foo] has non-unique elements"), Equals, true)
294+
}
295+
296+
func (s *RunSuite) TestValidationWithExtends(c *C) {
297+
template := `
298+
base:
299+
image: busybox
300+
privilege: "something"
301+
test:
302+
extends:
303+
service: base
304+
`
305+
306+
_, output := s.FromTextCaptureOutput(c, s.RandomProject(), "create", template)
307+
308+
c.Assert(strings.Contains(output, "Unsupported config option for base service: 'privilege' (did you mean 'privileged'?)"), Equals, true)
309+
310+
template = `
311+
base:
312+
image: busybox
313+
test:
314+
extends:
315+
service: base
316+
links: invalid_type
317+
`
318+
319+
_, output = s.FromTextCaptureOutput(c, s.RandomProject(), "create", template)
320+
321+
c.Assert(strings.Contains(output, "Service 'test' configuration key 'links' contains an invalid type, it should be an array"), Equals, true)
322+
323+
template = `
324+
test:
325+
extends:
326+
file: ./assets/validation/valid/docker-compose.yml
327+
service: base
328+
devices:
329+
- /dev/foo:/dev/foo
330+
- /dev/foo:/dev/foo
331+
`
332+
333+
_, output = s.FromTextCaptureOutput(c, s.RandomProject(), "create", template)
334+
335+
c.Assert(strings.Contains(output, "Service 'test' configuration key 'devices' value [/dev/foo:/dev/foo /dev/foo:/dev/foo] has non-unique elements"), Equals, true)
336+
337+
template = `
338+
test:
339+
extends:
340+
file: ./assets/validation/invalid/docker-compose.yml
341+
service: base
342+
`
343+
344+
_, output = s.FromTextCaptureOutput(c, s.RandomProject(), "create", template)
260345

346+
c.Assert(strings.Contains(output, "Service 'base' configuration key 'ports' contains an invalid type, it should be an array."), Equals, true)
261347
}

project/merge.go

+15
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func mergeProject(p *Project, file string, bytes []byte) (map[string]*ServiceCon
4343
return nil, err
4444
}
4545

46+
if err := validate(datas); err != nil {
47+
return nil, err
48+
}
49+
4650
for name, data := range datas {
4751
data, err := parse(p.context.ResourceLookup, p.context.EnvironmentLookup, file, data, datas)
4852
if err != nil {
@@ -62,6 +66,13 @@ func mergeProject(p *Project, file string, bytes []byte) (map[string]*ServiceCon
6266
datas[name] = data
6367
}
6468

69+
for name, data := range datas {
70+
err := validateServiceConstraints(data, name)
71+
if err != nil {
72+
return nil, err
73+
}
74+
}
75+
6576
if err := utils.Convert(datas, &configs); err != nil {
6677
return nil, err
6778
}
@@ -221,6 +232,10 @@ func parse(resourceLookup ResourceLookup, environmentLookup EnvironmentLookup, i
221232
return nil, err
222233
}
223234

235+
if err := validate(baseRawServices); err != nil {
236+
return nil, err
237+
}
238+
224239
baseService, ok = baseRawServices[service]
225240
if !ok {
226241
return nil, fmt.Errorf("Failed to find service %s in file %s", service, file)

project/merge_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func TestRestartNo(t *testing.T) {
158158

159159
config, err := mergeProject(p, "", []byte(`
160160
test:
161-
restart: no
161+
restart: "no"
162162
image: foo
163163
`))
164164

project/project_test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,13 @@ func TestParseWithMultipleComposeFiles(t *testing.T) {
163163
configTwo := []byte(`
164164
multiple:
165165
image: busybox
166-
name: multi
166+
container_name: multi
167167
ports:
168168
- 9000`)
169169

170170
configThree := []byte(`
171171
multiple:
172+
image: busybox
172173
mem_limit: 40000000
173174
ports:
174175
- 10000`)
@@ -182,7 +183,7 @@ func TestParseWithMultipleComposeFiles(t *testing.T) {
182183
assert.Nil(t, err)
183184

184185
assert.Equal(t, "busybox", p.Configs["multiple"].Image)
185-
assert.Equal(t, "multi", p.Configs["multiple"].Name)
186+
assert.Equal(t, "multi", p.Configs["multiple"].ContainerName)
186187
assert.Equal(t, []string{"8000", "9000"}, p.Configs["multiple"].Ports)
187188

188189
p = NewProject(&Context{
@@ -194,7 +195,7 @@ func TestParseWithMultipleComposeFiles(t *testing.T) {
194195
assert.Nil(t, err)
195196

196197
assert.Equal(t, "tianon/true", p.Configs["multiple"].Image)
197-
assert.Equal(t, "multi", p.Configs["multiple"].Name)
198+
assert.Equal(t, "multi", p.Configs["multiple"].ContainerName)
198199
assert.Equal(t, []string{"9000", "8000"}, p.Configs["multiple"].Ports)
199200

200201
p = NewProject(&Context{
@@ -206,7 +207,7 @@ func TestParseWithMultipleComposeFiles(t *testing.T) {
206207
assert.Nil(t, err)
207208

208209
assert.Equal(t, "busybox", p.Configs["multiple"].Image)
209-
assert.Equal(t, "multi", p.Configs["multiple"].Name)
210+
assert.Equal(t, "multi", p.Configs["multiple"].ContainerName)
210211
assert.Equal(t, []string{"8000", "9000", "10000"}, p.Configs["multiple"].Ports)
211212
assert.Equal(t, int64(40000000), p.Configs["multiple"].MemLimit)
212213
}

project/schema_helpers.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package project
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
7+
"github.com/docker/go-connections/nat"
8+
"github.com/xeipuuv/gojsonschema"
9+
)
10+
11+
var (
12+
schemaLoader gojsonschema.JSONLoader
13+
constraintSchemaLoader gojsonschema.JSONLoader
14+
schema map[string]interface{}
15+
)
16+
17+
type (
18+
environmentFormatChecker struct{}
19+
portsFormatChecker struct{}
20+
)
21+
22+
func (checker environmentFormatChecker) IsFormat(input string) bool {
23+
// If the value is a boolean, a warning should be given
24+
// However, we can't determine type since gojsonschema converts the value to a string
25+
// Adding a function with an interface{} parameter to gojsonschema is probably the best way to handle this
26+
return true
27+
}
28+
29+
func (checker portsFormatChecker) IsFormat(input string) bool {
30+
_, _, err := nat.ParsePortSpecs([]string{input})
31+
return err == nil
32+
}
33+
34+
func setupSchemaLoaders() error {
35+
if schema != nil {
36+
return nil
37+
}
38+
39+
var schemaRaw interface{}
40+
err := json.Unmarshal([]byte(schemaString), &schemaRaw)
41+
if err != nil {
42+
return err
43+
}
44+
45+
schema = schemaRaw.(map[string]interface{})
46+
47+
gojsonschema.FormatCheckers.Add("environment", environmentFormatChecker{})
48+
gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
49+
gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
50+
schemaLoader = gojsonschema.NewGoLoader(schemaRaw)
51+
52+
definitions := schema["definitions"].(map[string]interface{})
53+
constraints := definitions["constraints"].(map[string]interface{})
54+
service := constraints["service"].(map[string]interface{})
55+
constraintSchemaLoader = gojsonschema.NewGoLoader(service)
56+
57+
return nil
58+
}
59+
60+
// gojsonschema doesn't provide a list of valid types for a property
61+
// This parses the schema manually to find all valid types
62+
func parseValidTypesFromSchema(schema map[string]interface{}, context string) []string {
63+
contextSplit := strings.Split(context, ".")
64+
key := contextSplit[len(contextSplit)-1]
65+
66+
definitions := schema["definitions"].(map[string]interface{})
67+
service := definitions["service"].(map[string]interface{})
68+
properties := service["properties"].(map[string]interface{})
69+
property := properties[key].(map[string]interface{})
70+
71+
var validTypes []string
72+
73+
if val, ok := property["oneOf"]; ok {
74+
validConditions := val.([]interface{})
75+
76+
for _, validCondition := range validConditions {
77+
condition := validCondition.(map[string]interface{})
78+
validTypes = append(validTypes, condition["type"].(string))
79+
}
80+
} else if val, ok := property["$ref"]; ok {
81+
reference := val.(string)
82+
if reference == "#/definitions/string_or_list" {
83+
return []string{"string", "array"}
84+
} else if reference == "#/definitions/list_of_strings" {
85+
return []string{"array"}
86+
} else if reference == "#/definitions/list_or_dict" {
87+
return []string{"array", "object"}
88+
}
89+
}
90+
91+
return validTypes
92+
}

0 commit comments

Comments
 (0)