Skip to content

Commit 4b00593

Browse files
authored
Merge pull request #54 from klihub/devel/schema-based-validation
devel: perform schema-based Spec validation while loading Specs.
2 parents 4d9b881 + 2325a1f commit 4b00593

File tree

15 files changed

+842
-146
lines changed

15 files changed

+842
-146
lines changed

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ GO_VET := $(GO_CMD) vet
88

99
CDI_PKG := $(shell grep ^module go.mod | sed 's/^module *//g')
1010

11-
BINARIES := bin/cdi
11+
BINARIES := bin/cdi bin/validate
1212

1313
ifneq ($(V),1)
1414
Q := @
@@ -70,7 +70,7 @@ test-gopkgs:
7070
$(Q)$(GO_TEST) ./...
7171

7272
# tests for CDI Spec JSON schema
73-
test-schema:
73+
test-schema: bin/validate
7474
$(Q)echo "Building in schema..."; \
7575
$(MAKE) -C schema test
7676

@@ -79,6 +79,8 @@ test-schema:
7979
# dependencies
8080
#
8181

82+
bin/validate: cmd/validate/validate.go $(wildcard schema/*.json)
83+
8284
# quasi-automatic dependency for bin/cdi
8385
bin/cdi: $(wildcard cmd/cdi/*.go cmd/cdi/cmd/*.go) $(shell \
8486
for dir in \

cmd/cdi/cmd/cdi-api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ func cdiPrintRegistryErrors() {
345345
for path, specErrors := range cdiErrors {
346346
fmt.Printf("Spec file %s:\n", path)
347347
for idx, err := range specErrors {
348-
fmt.Printf(" %d: %v", idx, err)
348+
fmt.Printf(" %d: %v\n", idx, strings.TrimRight(err.Error(), "\n"))
349349
}
350350
}
351351
}

cmd/cdi/cmd/root.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717
package cmd
1818

1919
import (
20+
"fmt"
21+
"os"
22+
2023
"github.com/spf13/cobra"
2124

2225
"github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
2326
)
2427

25-
var specDirs []string
28+
var (
29+
specDirs []string
30+
schemaName string
31+
)
2632

2733
// rootCmd represents the base command when called without any subcommands
2834
var rootCmd = &cobra.Command{
@@ -48,9 +54,16 @@ func Execute() {
4854
func init() {
4955
cobra.OnInitialize(initSpecDirs)
5056
rootCmd.PersistentFlags().StringSliceVarP(&specDirs, "spec-dirs", "d", nil, "directories to scan for CDI Spec files")
57+
rootCmd.PersistentFlags().StringVarP(&schemaName, "schema", "s", "builtin", "JSON schema to use for validation")
5158
}
5259

5360
func initSpecDirs() {
61+
err := cdi.SetSchema(schemaName)
62+
if err != nil {
63+
fmt.Printf("failed to load JSON schema %s: %v\n", schemaName, err)
64+
os.Exit(1)
65+
}
66+
5467
if len(specDirs) > 0 {
5568
cdi.GetRegistry(cdi.WithSpecDirs(specDirs...))
5669
if len(cdi.GetRegistry().GetErrors()) > 0 {

cmd/cdi/cmd/validate.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ were reported by the registry.`,
4040
return
4141
}
4242

43-
fmt.Printf("CDI Registry has errors:\n")
44-
cdiPrintRegistryErrors()
45-
4643
os.Exit(1)
4744
},
4845
}

cmd/validate/validate.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Original code from: https://github.com/opencontainers/runtime-spec/blob/643c1429d905bba70fe977bae274f367ad101e73/schema/validate.go
3+
* Changes:
4+
* - Output errors to stderr
5+
* - Refactored to use package-internal validation library
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package main
21+
22+
import (
23+
"flag"
24+
"fmt"
25+
"io/ioutil"
26+
"os"
27+
28+
"github.com/container-orchestrated-devices/container-device-interface/schema"
29+
)
30+
31+
const usage = `Validate is used to check document with specified schema.
32+
You can use validate in following ways:
33+
34+
1.specify document file as an argument
35+
validate --schema <schema.json> <document.json>
36+
37+
2.pass document content through a pipe
38+
cat <document.json> | validate --schema <schema.json>
39+
40+
3.input document content manually, ended with ctrl+d(or your self-defined EOF keys)
41+
validate --schema <schema.json>
42+
[INPUT DOCUMENT CONTENT HERE]
43+
`
44+
45+
func main() {
46+
var (
47+
schemaFile string
48+
docFile string
49+
docData []byte
50+
err error
51+
exitCode int
52+
)
53+
54+
flag.Usage = func() {
55+
fmt.Fprintf(flag.CommandLine.Output(), "%s\n", usage)
56+
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
57+
flag.PrintDefaults()
58+
}
59+
60+
flag.StringVar(&schemaFile, "schema", "builtin", "JSON Schema to validate against")
61+
flag.Parse()
62+
63+
if schemaFile != "" {
64+
scm, err := schema.Load(schemaFile)
65+
if err != nil {
66+
fmt.Fprintf(os.Stderr, "failed to load schema %s: %v\n", schemaFile, err)
67+
os.Exit(1)
68+
}
69+
schema.Set(scm)
70+
fmt.Printf("Validating against JSON schema %s...\n", schemaFile)
71+
} else {
72+
fmt.Printf("Validating against builtin JSON schema...\n")
73+
}
74+
75+
docs := flag.Args()
76+
if len(docs) == 0 {
77+
docs = []string{"-"}
78+
}
79+
80+
for _, docFile = range docs {
81+
if docFile == "" || docFile == "-" {
82+
docFile = "<stdin>"
83+
docData, err = ioutil.ReadAll(os.Stdin)
84+
if err != nil {
85+
fmt.Fprintf(os.Stderr, "failed to read document data from stdin: %v\n", err)
86+
os.Exit(1)
87+
}
88+
err = schema.ValidateData(docData)
89+
} else {
90+
err = schema.ValidateFile(docFile)
91+
}
92+
93+
if err != nil {
94+
fmt.Fprintf(os.Stderr, "%s: validation failed:\n %v\n", docFile, err)
95+
exitCode = 1
96+
} else {
97+
fmt.Printf("%s: document is valid.\n", docFile)
98+
}
99+
}
100+
101+
os.Exit(exitCode)
102+
}

pkg/cdi/doc.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,24 @@
127127
// The default directories are '/etc/cdi' and '/var/run/cdi'. By putting
128128
// dynamically generated Spec files under '/var/run/cdi', those take
129129
// precedence over static ones in '/etc/cdi'.
130+
//
131+
// CDI Spec Validation
132+
//
133+
// This package performs both syntactic and semantic validation of CDI
134+
// Spec file data when a Spec file is loaded via the registry or using
135+
// the ReadSpec API function. As part of the semantic verification, the
136+
// Spec file is verified against the CDI Spec JSON validation schema.
137+
//
138+
// If a valid externally provided JSON validation schema is found in
139+
// the filesystem at /etc/cdi/schema/schema.json it is loaded and used
140+
// as the default validation schema. If such a file is not found or
141+
// fails to load, an embedded no-op schema is used.
142+
//
143+
// The used validation schema can also be changed programmatically using
144+
// the SetSchema API convenience function. This function also accepts
145+
// the special "builtin" (BuiltinSchemaName) and "none" (NoneSchemaName)
146+
// schema names which switch the used schema to the in-repo validation
147+
// schema embedded into the binary or the now default no-op schema
148+
// correspondingly. Other names are interpreted as the path to the actual
149+
/// validation schema to load and use.
130150
package cdi

pkg/cdi/schema.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright © 2022 The CDI Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cdi
18+
19+
import (
20+
"github.com/container-orchestrated-devices/container-device-interface/schema"
21+
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
22+
)
23+
24+
const (
25+
// DefaultExternalSchema is the JSON schema to load if found.
26+
DefaultExternalSchema = "/etc/cdi/schema/schema.json"
27+
)
28+
29+
// SetSchema sets the Spec JSON validation schema to use.
30+
func SetSchema(name string) error {
31+
s, err := schema.Load(name)
32+
if err != nil {
33+
return err
34+
}
35+
schema.Set(s)
36+
return nil
37+
}
38+
39+
// Validate CDI Spec against JSON Schema.
40+
func validateWithSchema(raw *cdi.Spec) error {
41+
return schema.ValidateType(raw)
42+
}
43+
44+
func init() {
45+
SetSchema(DefaultExternalSchema)
46+
}

pkg/cdi/spec.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,23 @@ func ReadSpec(path string, priority int) (*Spec, error) {
7272
return nil, errors.Errorf("failed to parse CDI Spec %q, no Spec data", path)
7373
}
7474

75-
return NewSpec(raw, path, priority)
75+
spec, err := NewSpec(raw, path, priority)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
return spec, nil
7681
}
7782

7883
// NewSpec creates a new Spec from the given CDI Spec data. The
7984
// Spec is marked as loaded from the given path with the given
8085
// priority. If Spec data validation fails NewSpec returns a nil
8186
// Spec and an error.
8287
func NewSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) {
83-
var err error
88+
err := validateWithSchema(raw)
89+
if err != nil {
90+
return nil, err
91+
}
8492

8593
spec := &Spec{
8694
Spec: raw,
@@ -178,11 +186,5 @@ func parseSpec(data []byte) (*cdi.Spec, error) {
178186
if err != nil {
179187
return nil, errors.Wrap(err, "failed to unmarshal CDI Spec")
180188
}
181-
return raw, validateJSONSchema(raw)
182-
}
183-
184-
// Validate CDI Spec against JSON Schema.
185-
func validateJSONSchema(raw *cdi.Spec) error {
186-
// TODO
187-
return nil
189+
return raw, nil
188190
}

pkg/cdi/spec_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ func TestNewSpec(t *testing.T) {
101101
name string
102102
data string
103103
unparsable bool
104+
schemaFail bool
104105
invalid bool
105106
}
106107
for _, tc := range []*testCase{
@@ -121,7 +122,7 @@ devices:
121122
env:
122123
- "FOO=BAR"
123124
`,
124-
invalid: true,
125+
schemaFail: true,
125126
},
126127
{
127128
name: "invalid, invalid CDI version",
@@ -146,7 +147,7 @@ devices:
146147
env:
147148
- "FOO=BAR"
148149
`,
149-
invalid: true,
150+
schemaFail: true,
150151
},
151152
{
152153
name: "invalid, invalid vendor",
@@ -175,14 +176,14 @@ devices:
175176
invalid: true,
176177
},
177178
{
178-
name: "invalid, invalid device",
179+
name: "invalid, missing required edits",
179180
data: `
180181
cdiVersion: "0.3.0"
181182
kind: vendor.com/device
182183
devices:
183184
- name: "dev1"
184185
`,
185-
invalid: true,
186+
schemaFail: true,
186187
},
187188
{
188189
name: "invalid, conflicting devices",
@@ -241,7 +242,7 @@ devices:
241242
require.NoError(t, err)
242243

243244
spec, err = NewSpec(raw, tc.name, 0)
244-
if tc.invalid {
245+
if tc.invalid || tc.schemaFail {
245246
require.Error(t, err)
246247
require.Nil(t, spec)
247248
return

schema/Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
build:
2-
go build ./validate.go
1+
VALIDATE ?= ../bin/validate
2+
SCHEMA ?= schema.json
33

4-
test: build
4+
test:
55
@FMT_RED=$$(tput setaf 1); \
66
FMT_BLUE=$$(tput setaf 12); \
77
FMT_CLEAR=$$(tput sgr0); \
88
echo "Running Good Tests"; \
99
for FILE in $$(ls "testdata/good"); do \
1010
FILE_PATH="testdata/good/$${FILE}"; \
11-
if ./validate "schema.json" "$${FILE_PATH}" > /dev/null ; then \
11+
if $(VALIDATE) --schema "$(SCHEMA)" "$${FILE_PATH}" > /dev/null ; then \
1212
printf '%s[OK]%s %s\n' "$${FMT_BLUE}" "$${FMT_CLEAR}" "$${FILE_PATH}"; \
1313
else \
1414
printf '%s[KO]%s %s\n' "$${FMT_RED}" "$${FMT_CLEAR}" "$${FILE_PATH}"; \
@@ -18,7 +18,7 @@ test: build
1818
echo "Running Bad Tests"; \
1919
for FILE in $$(ls "testdata/bad"); do \
2020
FILE_PATH="testdata/bad/$${FILE}"; \
21-
if ./validate "schema.json" "$${FILE_PATH}" > /dev/null ; then \
21+
if $(VALIDATE) --schema "$(SCHEMA)" "$${FILE_PATH}" > /dev/null ; then \
2222
printf '%s[KO]%s %s\n' "$${FMT_RED}" "$${FMT_CLEAR}" "$${FILE_PATH}"; \
2323
exit 1; \
2424
else \

0 commit comments

Comments
 (0)