Skip to content

Commit 0920c0e

Browse files
committed
Move all Kitfile validation into one place and fix path handling
* Merge split Kitfile validation (previously in pkg/lib/kitfile/validate.go and pkg/artifact/kitfile.go) into the artifact package * Fix handling of duplicate paths by checking against cleaned filepaths instead of raw strings * Add test cases for validation covering added validation processes Signed-off-by: Angel Misevski <amisevsk@gmail.com>
1 parent 72d307e commit 0920c0e

20 files changed

Lines changed: 137 additions & 144 deletions

pkg/artifact/kitfile.go

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,22 @@ import (
2222
"fmt"
2323
"io"
2424
"net/url"
25+
"path"
26+
"path/filepath"
27+
"regexp"
2528
"slices"
29+
"strings"
2630
"time"
2731

2832
modelspecv1 "github.com/modelpack/model-spec/specs-go/v1"
2933
"github.com/opencontainers/go-digest"
3034
"go.yaml.in/yaml/v3"
3135
)
3236

37+
const modelPartTypeMaxLen = 64
38+
39+
var modelPartTypeRegexp = regexp.MustCompile(`^[\w][\w.-]*$`)
40+
3341
// PathType represents different types of paths in Kitfile fields, e.g. local on-disk paths or remote S3 URLs.
3442
type PathType int
3543

@@ -164,79 +172,122 @@ func (kf *KitFile) MarshalToYAML() ([]byte, error) {
164172
}
165173

166174
func (kf *KitFile) Validate() error {
175+
var errs []string
176+
addErr := func(format string, a ...any) {
177+
s := fmt.Sprintf(format, a...)
178+
errs = append(errs, fmt.Sprintf(" * %s", s))
179+
}
167180
if kf.ManifestVersion != "1.0.0" {
168-
return fmt.Errorf("invalid manifestVersion: expect 1.0.0 but got %s", kf.ManifestVersion)
181+
addErr("invalid manifestVersion: expect 1.0.0 but got %s", kf.ManifestVersion)
182+
}
183+
184+
// Map of paths to the component that uses them; used to detect duplicate paths
185+
paths := map[string][]string{}
186+
addPath := func(path, source string) {
187+
if path == "" {
188+
path = "."
189+
}
190+
path = filepath.Clean(path)
191+
paths[path] = append(paths[path], source)
169192
}
170193

171194
if kf.Model != nil {
195+
addPath(kf.Model.Path, fmt.Sprintf("model %s", kf.Model.Name))
172196
modelPathType, err := GetPathType(kf.Model.Path)
173197
if err != nil {
174-
return fmt.Errorf("invalid path for model: %w", err)
198+
addErr("invalid path for model: %s", err)
175199
}
176200
if modelPathType != LocalPathType && modelPathType != ModelReferencePathType {
177-
return fmt.Errorf("invalid path for model: only local paths and ModelKit references are permitted")
201+
addErr("invalid path for model: only local paths and ModelKit references are permitted")
178202
}
179203
for _, part := range kf.Model.Parts {
204+
addPath(part.Path, fmt.Sprintf("modelpart %s", part.Name))
180205
partPathType, err := GetPathType(part.Path)
181206
if err != nil {
182-
return fmt.Errorf("invalid path for model part (%s): %w", part.Path, err)
207+
addErr("invalid path for model part (%s): %s", part.Path, err)
183208
}
184209
if partPathType != LocalPathType {
185-
return fmt.Errorf("invalid path for model part (%s): only local paths are permitted", part.Path)
210+
addErr("invalid path for model part (%s): only local paths are permitted", part.Path)
211+
}
212+
if part.Type != "" {
213+
if !modelPartTypeRegexp.MatchString(part.Type) {
214+
addErr("modelpart %s has invalid type (must be alphanumeric with dots, dashes, and underscores)", part.Name)
215+
}
216+
if len(part.Type) > modelPartTypeMaxLen {
217+
addErr("modelpart %s type is too long (must be fewer than %d characters)", part.Name, modelPartTypeMaxLen)
218+
}
186219
}
187220
}
188221
}
189-
for _, code := range kf.Code {
222+
for idx, code := range kf.Code {
223+
addPath(code.Path, fmt.Sprintf("code layer %d", idx))
190224
pathType, err := GetPathType(code.Path)
191225
if err != nil {
192-
return fmt.Errorf("invalid path for code (%s): %w", code.Path, err)
226+
addErr("invalid path for code (%s): %s", code.Path, err)
193227
}
194228
if pathType != LocalPathType {
195-
return fmt.Errorf("invalid path for code (%s): only local paths are permitted", code.Path)
229+
addErr("invalid path for code (%s): only local paths are permitted", code.Path)
196230
}
197231
}
198-
for _, dataset := range kf.DataSets {
232+
for idx, dataset := range kf.DataSets {
233+
addPath(dataset.Path, fmt.Sprintf("dataset layer %d", idx))
199234
pathType, err := GetPathType(dataset.Path)
200235
if err != nil {
201-
return fmt.Errorf("invalid path for dataset (%s): %w", dataset.Path, err)
236+
addErr("invalid path for dataset (%s): %s", dataset.Path, err)
202237
}
203238
if pathType != LocalPathType {
204-
return fmt.Errorf("invalid path for dataset (%s): only local paths are permitted", dataset.Path)
239+
addErr("invalid path for dataset (%s): only local paths are permitted", dataset.Path)
205240
}
206241
if dataset.RemotePath != "" {
207242
remotePathType, err := GetPathType(dataset.RemotePath)
208243
if err != nil {
209-
return fmt.Errorf("invalid remote path for dataset (%s): %w", dataset.RemotePath, err)
244+
addErr("invalid remote path for dataset (%s): %s", dataset.RemotePath, err)
210245
}
211246
if remotePathType != S3PathType {
212-
return fmt.Errorf("only S3 URLs are supported for remote dataset paths (%s)", dataset.RemotePath)
247+
addErr("only S3 URLs are supported for remote dataset paths (%s)", dataset.RemotePath)
213248
}
214249
if dataset.RemoteHash == "" {
215-
return fmt.Errorf("remoteHash is required when remote dataset paths are used (%s)", dataset.RemotePath)
250+
addErr("remoteHash is required when remote dataset paths are used (%s)", dataset.RemotePath)
216251
}
217252
} else {
218253
if dataset.RemoteHash != "" {
219-
return fmt.Errorf("remote hash is only applicable when remotePath is set")
254+
addErr("remote hash is only applicable when remotePath is set")
220255
}
221256
}
222257
}
223-
for _, doc := range kf.Docs {
258+
for idx, doc := range kf.Docs {
259+
addPath(doc.Path, fmt.Sprintf("docs layer %d", idx))
224260
pathType, err := GetPathType(doc.Path)
225261
if err != nil {
226-
return fmt.Errorf("invalid path for doc (%s): %w", doc.Path, err)
262+
addErr("invalid path for doc (%s): %s", doc.Path, err)
227263
}
228264
if pathType != LocalPathType {
229-
return fmt.Errorf("invalid path for doc (%s): only local paths are permitted", doc.Path)
265+
addErr("invalid path for doc (%s): only local paths are permitted", doc.Path)
230266
}
231267
}
232268
for _, prompt := range kf.Prompts {
233269
pathType, err := GetPathType(prompt.Path)
234270
if err != nil {
235-
return fmt.Errorf("invalid path for prompt (%s): %w", prompt.Path, err)
271+
addErr("invalid path for prompt (%s): %s", prompt.Path, err)
236272
}
237273
if pathType != LocalPathType {
238-
return fmt.Errorf("invalid path for prompt (%s): only local paths are permitted", prompt.Path)
274+
addErr("invalid path for prompt (%s): only local paths are permitted", prompt.Path)
275+
}
276+
}
277+
278+
for layerPath, layerIds := range paths {
279+
if len := len(layerIds); len > 1 {
280+
addErr("%s and %s use the same path %s", strings.Join(layerIds[:len-1], ", "), layerIds[len-1], layerPath)
239281
}
282+
if path.IsAbs(layerPath) || filepath.IsAbs(layerPath) {
283+
addErr("absolute paths are not supported in a Kitfile (path %s in %s)", layerPath, layerIds[0])
284+
}
285+
}
286+
287+
if len(errs) > 0 {
288+
// Iterating through the paths map is random; sort to get a consistent message
289+
slices.Sort(errs)
290+
return fmt.Errorf("errors while validating Kitfile: \n%s", strings.Join(errs, "\n"))
240291
}
241292

242293
return nil

pkg/artifact/kitfile_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (tc verifyTestCase) withName(name string) verifyTestCase {
7979
return tc
8080
}
8181

82-
func TestVerifyKitfile(t *testing.T) {
82+
func TestValidateKitfile(t *testing.T) {
8383
tests := loadAllTestCasesOrPanic[verifyTestCase](t, filepath.Join("testdata", "validation"))
8484
for _, tt := range tests {
8585
t.Run(tt.Name, func(t *testing.T) {
@@ -170,9 +170,9 @@ model:
170170
path: model-files
171171
license: "license-h"
172172
parts:
173-
- path: part-files
173+
- path: part-files-1
174174
license: "license-f"
175-
- path: part-files
175+
- path: part-files-2
176176
license: "license-e"
177177
datasets:
178178
- path: dataset
@@ -198,9 +198,9 @@ model:
198198
path: model-files
199199
license: MIT
200200
parts:
201-
- path: part-files
201+
- path: part-files-1
202202
license: Apache-2.0
203-
- path: part-files
203+
- path: part-files-2
204204
license: MIT
205205
datasets:
206206
- path: dataset

pkg/artifact/testdata/parameters/test-basic-parameters.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ kitfileYaml: |
55
name: test-kitfile
66
model:
77
name: test-model
8-
path: /tmp/path/to/model
8+
path: tmp/path/to/model
99
parameters:
1010
test-a: a
1111
test-b: b
1212
test-c: c
13-
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"/tmp/path/to/model","parameters":{"test-a":"a","test-b":"b","test-c":"c"}}}'
13+
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"tmp/path/to/model","parameters":{"test-a":"a","test-b":"b","test-c":"c"}}}'

pkg/artifact/testdata/parameters/test-data-types.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ kitfileYaml: |
55
name: test-kitfile
66
model:
77
name: test-model
8-
path: /tmp/path/to/model
8+
path: tmp/path/to/model
99
parameters:
1010
test-bool: true
1111
test-bool-str: "true"
1212
test-float: 12.3
1313
test-negative: -123
1414
test-number: 123
1515
test-string: string
16-
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"/tmp/path/to/model","parameters":{"test-bool":true,"test-bool-str":"true","test-float":12.3,"test-negative":-123,"test-number":123,"test-string":"string"}}}'
16+
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"tmp/path/to/model","parameters":{"test-bool":true,"test-bool-str":"true","test-float":12.3,"test-negative":-123,"test-number":123,"test-string":"string"}}}'

pkg/artifact/testdata/parameters/test-list-parameters.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ kitfileYaml: |
55
name: test-kitfile
66
model:
77
name: test-model
8-
path: /tmp/path/to/model
8+
path: tmp/path/to/model
99
parameters:
1010
- item-a
1111
- item-b
1212
- item-c
13-
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"/tmp/path/to/model","parameters":["item-a","item-b","item-c"]}}'
13+
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"tmp/path/to/model","parameters":["item-a","item-b","item-c"]}}'

pkg/artifact/testdata/parameters/test-nested-parameters.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ kitfileYaml: |
55
name: test-kitfile
66
model:
77
name: test-model
8-
path: /tmp/path/to/model
8+
path: tmp/path/to/model
99
parameters:
1010
test-list-of-lists:
1111
- - a-1
@@ -28,4 +28,4 @@ kitfileYaml: |
2828
test-a: a
2929
test-b: b
3030
test-c: c
31-
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"/tmp/path/to/model","parameters":{"test-list-of-lists":[["a-1","a-2"],["b-1","b-2"]],"test-maps":{"a":{"b":{"c":{"d":{"e":"f"}}}}},"test-nested-list":["item-a","item-b",{"item-object":[{"subfield-1":1},{"subfield-2":2}]}],"test-nested-map":{"test-a":"a","test-b":"b","test-c":"c"}}}}'
31+
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"tmp/path/to/model","parameters":{"test-list-of-lists":[["a-1","a-2"],["b-1","b-2"]],"test-maps":{"a":{"b":{"c":{"d":{"e":"f"}}}}},"test-nested-list":["item-a","item-b",{"item-object":[{"subfield-1":1},{"subfield-2":2}]}],"test-nested-map":{"test-a":"a","test-b":"b","test-c":"c"}}}}'

pkg/artifact/testdata/parameters/test-single-value.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ kitfileYaml: |
55
name: test-kitfile
66
model:
77
name: test-model
8-
path: /tmp/path/to/model
8+
path: tmp/path/to/model
99
parameters: |
1010
test-string
11-
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"/tmp/path/to/model","parameters":"test-string\n"}}'
11+
kitfileJson: '{"manifestVersion":"1.0.0","package":{"name":"test-kitfile"},"model":{"name":"test-model","path":"tmp/path/to/model","parameters":"test-string\n"}}'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kitfile: |
2+
manifestVersion: 1.0.0
3+
datasets:
4+
- path: test-path
5+
code:
6+
- path: test-path
7+
errRegexp: "use the same path test-path"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kitfile: |
2+
manifestVersion: 1.0.0
3+
model:
4+
path: test-path
5+
code:
6+
- path: test-path
7+
errRegexp: "use the same path test-path"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kitfile: |
2+
manifestVersion: 1.0.0
3+
model:
4+
path: test-path/
5+
datasets:
6+
- path: test-path
7+
errRegexp: "use the same path test-path"

0 commit comments

Comments
 (0)