@@ -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.
3442type PathType int
3543
@@ -164,79 +172,122 @@ func (kf *KitFile) MarshalToYAML() ([]byte, error) {
164172}
165173
166174func (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
0 commit comments