Skip to content

Commit fa0e5c3

Browse files
committed
Handle cases where artifact is recognized but does not contain Kitfile
Since we're adding support for model-spec artifacts (modelpacks), there may be cases where we are handling artifacts that do not have a Kitfile available. As a result, we need to handle the case where a config (Kitfile) cannot be found: * Add error type for repo utilities to signify that everything is fine, except no Kitfile is available * For inspect, warn that there is no Kitfile and show the manifest * For list, ignore metadata that we normally retrieve from the Kitfile (e.g. author and model name) * For unpack, attempt to generate a dummy config to allow unpacking to continue: * Don't unpack Kitfile, as it does not exist * Use model-spec file path annotations to fill a fake Kitfile with paths to allow unpacking Signed-off-by: Angel Misevski <[email protected]>
1 parent d97e721 commit fa0e5c3

File tree

10 files changed

+185
-20
lines changed

10 files changed

+185
-20
lines changed

pkg/cmd/inspect/inspect.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package inspect
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223

2324
"github.com/kitops-ml/kitops/pkg/artifact"
@@ -66,7 +67,7 @@ func getRemoteInspect(ctx context.Context, opts *inspectOptions) (*inspectInfo,
6667

6768
func getInspectInfo(ctx context.Context, repository oras.Target, ref string) (*inspectInfo, error) {
6869
desc, manifest, kitfile, err := util.ResolveManifestAndConfig(ctx, repository, ref)
69-
if err != nil {
70+
if err != nil && !errors.Is(err, util.ErrNoKitfile) {
7071
return nil, err
7172
}
7273
version := "unknown"

pkg/cmd/list/list.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,17 @@ func readInfoFromRepo(ctx context.Context, repo local.LocalRepo) ([]modelInfo, e
4949
var infos []modelInfo
5050
manifestDescs := repo.GetAllModels()
5151
for _, manifestDesc := range manifestDescs {
52-
manifest, config, err := util.GetManifestAndConfig(ctx, repo, manifestDesc)
53-
if err != nil && !errors.Is(err, util.ErrNotAModelKit) {
54-
return nil, err
52+
manifest, config, err := util.GetManifestAndKitfile(ctx, repo, manifestDesc)
53+
if err != nil {
54+
if errors.Is(err, util.ErrNotAModelKit) {
55+
// Shouldn't happen since this is a local repo, but either way it's not a supported artifact
56+
continue
57+
}
58+
// Allow artifacts without Kitfiles as all that will be lacking is some metadata; we can still
59+
// describe them
60+
if !errors.Is(err, util.ErrNoKitfile) {
61+
return nil, err
62+
}
5563
}
5664
tags := repo.GetTags(manifestDesc)
5765
// Strip localhost from repo if present, since we added it

pkg/cmd/list/remote.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ func listImageTag(ctx context.Context, repo registry.Repository, ref *registry.R
7777
if err != nil {
7878
return nil, fmt.Errorf("failed to resolve reference %s: %w", ref.Reference, err)
7979
}
80-
manifest, config, err := util.GetManifestAndConfig(ctx, repo, manifestDesc)
81-
if err != nil {
80+
manifest, config, err := util.GetManifestAndKitfile(ctx, repo, manifestDesc)
81+
if err != nil && !errors.Is(err, util.ErrNoKitfile) {
8282
return nil, fmt.Errorf("failed to read modelkit: %w", err)
8383
}
8484
if _, err := mediatype.ModelFormatForManifest(manifest); err != nil {

pkg/cmd/list/util.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ func (m *modelInfo) format() []string {
5252
return lines
5353
}
5454

55+
// fill adds information pulled from a manifest and kitfile into the modelInfo. Handles
56+
// cases where the Kitfile is nil
5557
func (m *modelInfo) fill(manifest *ocispec.Manifest, kitfile *artifact.KitFile) {
5658
m.Size = getModelSize(manifest)
5759
m.Author = getModelAuthor(kitfile)
@@ -67,15 +69,18 @@ func getModelSize(manifest *ocispec.Manifest) string {
6769
}
6870

6971
func getModelAuthor(kitfile *artifact.KitFile) string {
70-
if len(kitfile.Package.Authors) > 0 {
72+
if kitfile != nil && len(kitfile.Package.Authors) > 0 {
7173
return kitfile.Package.Authors[0]
7274
} else {
7375
return "<none>"
7476
}
7577
}
7678

7779
func getModelName(kitfile *artifact.KitFile) string {
78-
name := kitfile.Package.Name
80+
name := ""
81+
if kitfile != nil {
82+
name = kitfile.Package.Name
83+
}
7984
if name == "" {
8085
name = "<none>"
8186
}

pkg/cmd/list/util_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 The KitOps Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
package list
18+
19+
import (
20+
"testing"
21+
22+
"github.com/kitops-ml/kitops/pkg/artifact"
23+
"github.com/opencontainers/image-spec/specs-go"
24+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
25+
"github.com/stretchr/testify/assert"
26+
)
27+
28+
func TestModelInfoFillNormal(t *testing.T) {
29+
info := modelInfo{}
30+
manifest := genTestManifest(100, 200, 300)
31+
kitfile := &artifact.KitFile{
32+
Package: artifact.Package{
33+
Name: "testmodelkit",
34+
Authors: []string{"testauthor1", "testauthor2"},
35+
},
36+
}
37+
info.fill(manifest, kitfile)
38+
// Not testing size formatting here
39+
assert.Equal(t, info.Size, "600 B")
40+
// Should use first author in list
41+
assert.Equal(t, info.Author, "testauthor1")
42+
assert.Equal(t, info.ModelName, "testmodelkit")
43+
}
44+
45+
func TestModelInfoFillEmptyKitfile(t *testing.T) {
46+
info := modelInfo{}
47+
manifest := genTestManifest(100, 200, 300)
48+
kitfile := &artifact.KitFile{}
49+
info.fill(manifest, kitfile)
50+
// Not testing size formatting here
51+
assert.Equal(t, info.Size, "600 B")
52+
// Should use first author in list
53+
assert.Equal(t, info.Author, "<none>")
54+
assert.Equal(t, info.ModelName, "<none>")
55+
}
56+
57+
func TestModelInfoFillNilKitfile(t *testing.T) {
58+
info := modelInfo{}
59+
manifest := genTestManifest(100, 200, 300)
60+
info.fill(manifest, nil)
61+
// Not testing size formatting here
62+
assert.Equal(t, info.Size, "600 B")
63+
// Should use first author in list
64+
assert.Equal(t, info.Author, "<none>")
65+
assert.Equal(t, info.ModelName, "<none>")
66+
}
67+
68+
func genTestManifest(layersizes ...int64) *ocispec.Manifest {
69+
var layers []ocispec.Descriptor
70+
for _, s := range layersizes {
71+
layers = append(layers, ocispec.Descriptor{
72+
// Empty media type is sufficient for now as we don't care about mediatypes
73+
// when filling
74+
MediaType: ocispec.DescriptorEmptyJSON.MediaType,
75+
Digest: ocispec.DescriptorEmptyJSON.Digest,
76+
Size: s,
77+
})
78+
}
79+
return &ocispec.Manifest{
80+
Versioned: specs.Versioned{SchemaVersion: 2},
81+
MediaType: ocispec.MediaTypeImageManifest,
82+
Config: ocispec.DescriptorEmptyJSON,
83+
Layers: layers,
84+
}
85+
}

pkg/cmd/pull/pull.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package pull
1919
import (
2020
"context"
2121
"encoding/json"
22+
"errors"
2223
"fmt"
2324
"io"
2425
"strings"
@@ -68,8 +69,12 @@ func runPullRecursive(ctx context.Context, localRepo local.LocalRepo, opts *pull
6869
}
6970

7071
func pullParents(ctx context.Context, localRepo local.LocalRepo, desc ocispec.Descriptor, optsIn *pullOptions, pulledRefs []string) error {
71-
_, config, err := util.GetManifestAndConfig(ctx, localRepo, desc)
72+
_, config, err := util.GetManifestAndKitfile(ctx, localRepo, desc)
7273
if err != nil {
74+
if errors.Is(err, util.ErrNoKitfile) {
75+
// If there's no Kitfile but it's otherwise a support artifact type, skip pulling parents as there aren't any
76+
return nil
77+
}
7378
return err
7479
}
7580
if config.Model == nil || !util.IsModelKitReference(config.Model.Path) {

pkg/lib/filesystem/unpack/core.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/kitops-ml/kitops/pkg/lib/repo/util"
3636
"github.com/kitops-ml/kitops/pkg/output"
3737

38+
modelspecv1 "github.com/modelpack/model-spec/specs-go/v1"
3839
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3940
"oras.land/oras-go/v2/content"
4041
)
@@ -87,9 +88,19 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str
8788
}
8889
config, err := util.GetKitfileForManifest(ctx, store, manifest)
8990
if err != nil {
90-
output.Infof("Could not get Kitfile: %s", err)
91-
}
92-
if config != nil {
91+
if !errors.Is(err, util.ErrNoKitfile) {
92+
return err
93+
}
94+
output.Logf(output.LogLevelWarn, "Could not get Kitfile: %s", err)
95+
output.Logf(output.LogLevelWarn, "Functionality may be impacted")
96+
// TODO: we can probably _also_ handle getting the model-spec config and using it here
97+
genconfig, err := generateKitfileForModelPack(manifest)
98+
if err != nil {
99+
return fmt.Errorf("could not process manifest: %w", err)
100+
}
101+
config = genconfig
102+
} else {
103+
// These steps only make sense if we have a legitimate Kitfile available
93104
if config.Model != nil && util.IsModelKitReference(config.Model.Path) {
94105
output.Infof("Unpacking referenced modelkit %s", config.Model.Path)
95106
if err := unpackParent(ctx, config.Model.Path, opts, visitedRefs); err != nil {
@@ -196,6 +207,7 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str
196207
}
197208
}
198209

210+
// TODO: handle DiffIDs when unpacking layers
199211
if err := unpackLayer(ctx, store, layerDesc, relPath, opts.Overwrite, opts.IgnoreExisting, mediaType.Compression()); err != nil {
200212
return fmt.Errorf("failed to unpack: %w", err)
201213
}
@@ -396,3 +408,40 @@ func getIndex(list []string, s string) int {
396408
}
397409
return -1
398410
}
411+
412+
// generateKitfileForModelPack generates a "Kitfile" for a manifest that otherwise does not contain one.
413+
// This is a minimal Kitfile suitable only for unpacking, containing a path for every layer. If a layer
414+
// does not use the 'org.cncf.model.filepath' annotation, an error is returned.
415+
func generateKitfileForModelPack(manifest *ocispec.Manifest) (*artifact.KitFile, error) {
416+
if format, err := mediatype.ModelFormatForManifest(manifest); err != nil || format != mediatype.ModelPackFormat {
417+
return nil, fmt.Errorf("not a modelpack artifact")
418+
}
419+
kf := &artifact.KitFile{
420+
Model: &artifact.Model{},
421+
}
422+
for _, desc := range manifest.Layers {
423+
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
424+
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
425+
}
426+
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
427+
mt, err := mediatype.ParseMediaType(desc.MediaType)
428+
if err != nil {
429+
return nil, err
430+
}
431+
switch mt.Base() {
432+
case mediatype.ModelBaseType:
433+
kf.Model.Path = filepath
434+
case mediatype.ModelPartBaseType:
435+
kf.Model.Parts = append(kf.Model.Parts, artifact.ModelPart{Path: filepath})
436+
case mediatype.CodeBaseType:
437+
kf.Code = append(kf.Code, artifact.Code{Path: filepath})
438+
case mediatype.DatasetBaseType:
439+
kf.DataSets = append(kf.DataSets, artifact.DataSet{Path: filepath})
440+
case mediatype.DocsBaseType:
441+
kf.Docs = append(kf.Docs, artifact.Docs{Path: filepath})
442+
default:
443+
return nil, fmt.Errorf("unknown layer type: %s", mt)
444+
}
445+
}
446+
return kf, nil
447+
}

pkg/lib/kitfile/resolve.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package kitfile
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"strings"
2324

@@ -49,6 +50,9 @@ func GetKitfileForRef(ctx context.Context, configHome string, ref *registry.Refe
4950
_, _, localKitfile, err := util.ResolveManifestAndConfig(ctx, localRepo, ref.Reference)
5051
if err == nil {
5152
return localKitfile, nil
53+
} else if errors.Is(err, util.ErrNoKitfile) || errors.Is(err, util.ErrNotAModelKit) {
54+
// We found an artifact but it does not have a Kitfile
55+
return nil, err
5256
}
5357

5458
repository, err := remote.NewRepository(ctx, ref.Registry, ref.Repository, options.DefaultNetworkOptions(configHome))

pkg/lib/repo/util/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ package util
1919
import "errors"
2020

2121
var ErrNotAModelKit = errors.New("reference exists but is not a modelkit")
22+
var ErrNoKitfile = errors.New("artifact does not contain a Kitfile")

pkg/lib/repo/util/reference.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,16 +176,17 @@ func RepoPath(storagePath string, ref *registry.Reference) string {
176176
return filepath.Join(storagePath, ref.Registry, ref.Repository)
177177
}
178178

179-
// GetManifestAndConfig returns the manifest and config (Kitfile) for a manifest Descriptor.
180-
// Calls GetManifest and GetConfig.
181-
func GetManifestAndConfig(ctx context.Context, store oras.ReadOnlyTarget, manifestDesc ocispec.Descriptor) (*ocispec.Manifest, *artifact.KitFile, error) {
179+
// GetManifestAndKitfile returns the manifest and config (Kitfile) for a manifest Descriptor.
180+
// Calls GetManifest and GetKitfileForManifest. If the manifest is retrieved but no Kitfile
181+
// can be found, returns the manifest and error equal to ErrNoKitfile
182+
func GetManifestAndKitfile(ctx context.Context, store oras.ReadOnlyTarget, manifestDesc ocispec.Descriptor) (*ocispec.Manifest, *artifact.KitFile, error) {
182183
manifest, err := GetManifest(ctx, store, manifestDesc)
183184
if err != nil {
184185
return nil, nil, err
185186
}
186187
config, err := GetKitfileForManifest(ctx, store, manifest)
187188
if err != nil {
188-
return nil, nil, err
189+
return manifest, nil, err
189190
}
190191
return manifest, config, nil
191192
}
@@ -208,6 +209,10 @@ func GetManifest(ctx context.Context, store oras.ReadOnlyTarget, manifestDesc oc
208209
return manifest, nil
209210
}
210211

212+
// GetKitfileForManifest returns the Kitfile for a given manifest, either by retrieving it from an
213+
// OCI store or by reading it from manifest annotations. If manifest type is unrecognized, returns
214+
// ErrNotAModelKit. If the manifest is recognized but does not contain a Kitfile (e.g. it was not
215+
// created by Kit), returns ErrNoKitfile.
211216
func GetKitfileForManifest(ctx context.Context, store oras.ReadOnlyTarget, manifest *ocispec.Manifest) (*artifact.KitFile, error) {
212217
modelFormat, err := mediatype.ModelFormatForManifest(manifest)
213218
if err != nil {
@@ -219,7 +224,7 @@ func GetKitfileForManifest(ctx context.Context, store oras.ReadOnlyTarget, manif
219224
case mediatype.ModelPackFormat:
220225
// TODO: can we (try to) generate a Kitfile from a ModelPack manifest?
221226
if manifest.Annotations == nil || manifest.Annotations[constants.KitfileJsonAnnotation] == "" {
222-
return nil, fmt.Errorf("ModelPack artifact does not contain a Kitfile")
227+
return nil, ErrNoKitfile
223228
}
224229
kfstring := manifest.Annotations[constants.KitfileJsonAnnotation]
225230
kitfile := &artifact.KitFile{}
@@ -228,7 +233,8 @@ func GetKitfileForManifest(ctx context.Context, store oras.ReadOnlyTarget, manif
228233
}
229234
return kitfile, nil
230235
default:
231-
return nil, fmt.Errorf("Unknown artifact type")
236+
// Won't happen but necessary for completeness
237+
return nil, fmt.Errorf("unknown artifact type")
232238
}
233239
}
234240

@@ -266,15 +272,16 @@ func ResolveManifest(ctx context.Context, store oras.Target, reference string) (
266272
}
267273

268274
// ResolveManifestAndConfig returns the manifest and config (Kitfile) for a given reference (tag), if present
269-
// in the store. Calls ResolveManifest and GetConfig.
275+
// in the store. Calls GetManifest and GetKitfileForManifest. If the manifest is retrieved but no Kitfile
276+
// can be found, returns the manifest and error equal to ErrNoKitfile
270277
func ResolveManifestAndConfig(ctx context.Context, store oras.Target, reference string) (ocispec.Descriptor, *ocispec.Manifest, *artifact.KitFile, error) {
271278
desc, manifest, err := ResolveManifest(ctx, store, reference)
272279
if err != nil {
273280
return ocispec.DescriptorEmptyJSON, nil, nil, err
274281
}
275282
config, err := GetKitfileForManifest(ctx, store, manifest)
276283
if err != nil {
277-
return ocispec.DescriptorEmptyJSON, nil, nil, err
284+
return desc, manifest, nil, err
278285
}
279286
return desc, manifest, config, nil
280287
}

0 commit comments

Comments
 (0)