Skip to content

Commit 60ab78a

Browse files
add filter support to model catalog (kubeflow#1436)
* add filter support to model catalog Signed-off-by: Adysen Rothman <[email protected]> * add filter support to yaml Signed-off-by: Adysen Rothman <[email protected]> * update tests Signed-off-by: Adysen Rothman <[email protected]> * more clear model verbiage & logging Signed-off-by: Adysen Rothman <[email protected]> * rm sample rhec catalog --> example in readme Signed-off-by: Adysen Rothman <[email protected]> --------- Signed-off-by: Adysen Rothman <[email protected]>
1 parent 36e658f commit 60ab78a

File tree

8 files changed

+176
-42
lines changed

8 files changed

+176
-42
lines changed

catalog/internal/catalog/catalog_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestLoadCatalogSources(t *testing.T) {
2121
{
2222
name: "test-catalog-sources",
2323
args: args{catalogsPath: "testdata/test-catalog-sources.yaml"},
24-
want: []string{"catalog1", "catalog3"},
24+
want: []string{"catalog1", "catalog3", "catalog4"},
2525
wantErr: false,
2626
},
2727
}
@@ -69,6 +69,11 @@ func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) {
6969
Name: "Catalog 3",
7070
Enabled: &trueValue,
7171
},
72+
"catalog4": {
73+
Id: "catalog4",
74+
Name: "Catalog 4",
75+
Enabled: &trueValue,
76+
},
7277
},
7378
wantErr: false,
7479
},

catalog/internal/catalog/rhec_catalog.go

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"time"
1313

1414
"github.com/Khan/genqlient/graphql"
15-
"github.com/golang/glog"
1615
"github.com/kubeflow/model-registry/catalog/internal/catalog/genqlient"
1716
"github.com/kubeflow/model-registry/catalog/pkg/openapi"
1817
model "github.com/kubeflow/model-registry/catalog/pkg/openapi"
@@ -26,9 +25,8 @@ type rhecModel struct {
2625

2726
// rhecCatalogConfig defines the structure of the RHEC catalog configuration.
2827
type rhecCatalogConfig struct {
29-
Models []struct {
30-
Repository string `yaml:"repository"`
31-
} `yaml:"models"`
28+
Models []string `yaml:"models"`
29+
ExcludedModels []string `yaml:"excludedModels"`
3230
}
3331

3432
type rhecCatalogImpl struct {
@@ -229,23 +227,12 @@ func newRhecModel(repoData *genqlient.GetRepositoryResponse, imageData genqlient
229227
}
230228
}
231229

232-
func (r *rhecCatalogImpl) load(modelsList []any) error {
230+
func (r *rhecCatalogImpl) load(modelsList []string, excludedModelsList []string) error {
233231
graphqlClient := graphql.NewClient("https://catalog.redhat.com/api/containers/graphql/", http.DefaultClient)
234232
ctx := context.Background()
235233

236234
models := make(map[string]*rhecModel)
237-
for _, modelEntry := range modelsList {
238-
modelMap, ok := modelEntry.(map[string]any)
239-
if !ok {
240-
glog.Warningf("skipping invalid entry in 'models' list")
241-
continue
242-
}
243-
repo, ok := modelMap["repository"].(string)
244-
if !ok {
245-
glog.Warningf("skipping model with missing or invalid 'repository'")
246-
continue
247-
}
248-
235+
for _, repo := range modelsList {
249236
repoData, err := fetchRepository(ctx, graphqlClient, repo)
250237
if err != nil {
251238
return err
@@ -261,6 +248,11 @@ func (r *rhecCatalogImpl) load(modelsList []any) error {
261248
for _, imageTag := range imageRepository.Tags {
262249
tagName := imageTag.Name
263250
fullModelName := repo + ":" + tagName
251+
252+
if isModelExcluded(fullModelName, excludedModelsList) {
253+
continue
254+
}
255+
264256
model := newRhecModel(repoData, image, tagName, repo)
265257
models[fullModelName] = model
266258
}
@@ -275,6 +267,19 @@ func (r *rhecCatalogImpl) load(modelsList []any) error {
275267
return nil
276268
}
277269

270+
func isModelExcluded(modelName string, patterns []string) bool {
271+
for _, pattern := range patterns {
272+
if strings.HasSuffix(pattern, "*") {
273+
if strings.HasPrefix(modelName, strings.TrimSuffix(pattern, "*")) {
274+
return true
275+
}
276+
} else if modelName == pattern {
277+
return true
278+
}
279+
}
280+
return false
281+
}
282+
278283
func newRhecCatalog(source *CatalogSourceConfig) (CatalogSourceProvider, error) {
279284
modelsData, ok := source.Properties["models"]
280285
if !ok {
@@ -286,11 +291,35 @@ func newRhecCatalog(source *CatalogSourceConfig) (CatalogSourceProvider, error)
286291
return nil, fmt.Errorf("'models' property should be a list")
287292
}
288293

294+
models := make([]string, len(modelsList))
295+
for i, v := range modelsList {
296+
models[i], ok = v.(string)
297+
if !ok {
298+
return nil, fmt.Errorf("invalid entry in 'models' list, expected a string")
299+
}
300+
}
301+
302+
// Excluded models is an optional source property.
303+
var excludedModels []string
304+
if excludedModelsData, ok := source.Properties["excludedModels"]; ok {
305+
excludedModelsList, ok := excludedModelsData.([]any)
306+
if !ok {
307+
return nil, fmt.Errorf("'excludedModels' property should be a list")
308+
}
309+
excludedModels = make([]string, len(excludedModelsList))
310+
for i, v := range excludedModelsList {
311+
excludedModels[i], ok = v.(string)
312+
if !ok {
313+
return nil, fmt.Errorf("invalid entry in 'excludedModels' list, expected a string")
314+
}
315+
}
316+
}
317+
289318
r := &rhecCatalogImpl{
290319
models: make(map[string]*rhecModel),
291320
}
292321

293-
err := r.load(modelsList)
322+
err := r.load(models, excludedModels)
294323
if err != nil {
295324
return nil, fmt.Errorf("error loading rhec catalog: %w", err)
296325
}

catalog/internal/catalog/rhec_catalog_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,52 @@ func TestRhecCatalogListModels(t *testing.T) {
342342
})
343343
}
344344
}
345+
346+
func TestIsModelExcluded(t *testing.T) {
347+
tests := []struct {
348+
name string
349+
modelName string
350+
patterns []string
351+
want bool
352+
}{
353+
{
354+
name: "exact match",
355+
modelName: "model1:v1",
356+
patterns: []string{"model1:v1"},
357+
want: true,
358+
},
359+
{
360+
name: "wildcard match",
361+
modelName: "model1:v2",
362+
patterns: []string{"model1:*"},
363+
want: true,
364+
},
365+
{
366+
name: "no match",
367+
modelName: "model2:v1",
368+
patterns: []string{"model1:*"},
369+
want: false,
370+
},
371+
{
372+
name: "multiple patterns with match",
373+
modelName: "model3:v1",
374+
patterns: []string{"model2:*", "model3:v1"},
375+
want: true,
376+
},
377+
{
378+
name: "empty patterns",
379+
modelName: "model1:v1",
380+
patterns: []string{},
381+
want: false,
382+
},
383+
}
384+
385+
for _, tt := range tests {
386+
t.Run(tt.name, func(t *testing.T) {
387+
got := isModelExcluded(tt.modelName, tt.patterns)
388+
if got != tt.want {
389+
t.Errorf("isModelExcluded() = %v, want %v", got, tt.want)
390+
}
391+
})
392+
}
393+
}

catalog/internal/catalog/testdata/test-catalog-sources.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,13 @@ catalogs:
2121
enabled: true
2222
properties:
2323
models:
24-
- repository: rhelai1/modelcar-granite-7b-starter
24+
- rhelai1/modelcar-granite-7b-starter
25+
- name: "Catalog 4"
26+
id: catalog4
27+
type: rhec
28+
enabled: true
29+
properties:
30+
models:
31+
- rhelai1/modelcar-granite-7b-starter
32+
excludedModels:
33+
- rhelai1/modelcar-granite-7b-starter:latest

catalog/internal/catalog/yaml_catalog.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func (y *yamlCatalogImpl) GetArtifacts(ctx context.Context, name string) (*model
150150
return &list, nil
151151
}
152152

153-
func (y *yamlCatalogImpl) load(path string) error {
153+
func (y *yamlCatalogImpl) load(path string, excludedModelsList []string) error {
154154
bytes, err := os.ReadFile(path)
155155
if err != nil {
156156
return fmt.Errorf("failed to read %s file: %v", yamlCatalogPath, err)
@@ -161,9 +161,13 @@ func (y *yamlCatalogImpl) load(path string) error {
161161
return fmt.Errorf("failed to parse %s file: %v", yamlCatalogPath, err)
162162
}
163163

164-
models := make(map[string]*yamlModel, len(contents.Models))
164+
models := make(map[string]*yamlModel)
165165
for i := range contents.Models {
166-
models[contents.Models[i].Name] = &contents.Models[i]
166+
modelName := contents.Models[i].Name
167+
if isModelExcluded(modelName, excludedModelsList) {
168+
continue
169+
}
170+
models[modelName] = &contents.Models[i]
167171
}
168172

169173
y.modelsLock.Lock()
@@ -186,8 +190,26 @@ func newYamlCatalog(source *CatalogSourceConfig) (CatalogSourceProvider, error)
186190
return nil, fmt.Errorf("abs: %w", err)
187191
}
188192

189-
p := &yamlCatalogImpl{}
190-
err = p.load(yamlModelFile)
193+
// Excluded models is an optional source property.
194+
var excludedModels []string
195+
if excludedModelsData, ok := source.Properties["excludedModels"]; ok {
196+
excludedModelsList, ok := excludedModelsData.([]any)
197+
if !ok {
198+
return nil, fmt.Errorf("'excludedModels' property should be a list")
199+
}
200+
excludedModels = make([]string, len(excludedModelsList))
201+
for i, v := range excludedModelsList {
202+
excludedModels[i], ok = v.(string)
203+
if !ok {
204+
return nil, fmt.Errorf("invalid entry in 'excludedModels' list, expected a string")
205+
}
206+
}
207+
}
208+
209+
p := &yamlCatalogImpl{
210+
models: make(map[string]*yamlModel),
211+
}
212+
err = p.load(yamlModelFile, excludedModels)
191213
if err != nil {
192214
return nil, err
193215
}
@@ -202,7 +224,7 @@ func newYamlCatalog(source *CatalogSourceConfig) (CatalogSourceProvider, error)
202224
for range changes {
203225
glog.Infof("Reloading YAML catalog %s", yamlModelFile)
204226

205-
err = p.load(yamlModelFile)
227+
err = p.load(yamlModelFile, excludedModels)
206228
if err != nil {
207229
glog.Errorf("unable to load YAML catalog: %v", err)
208230
}

catalog/internal/catalog/yaml_catalog_test.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,16 +171,37 @@ func TestYAMLCatalogListModels(t *testing.T) {
171171
assert.Equal(int32(0), emptyModels.Size)
172172
assert.Equal(int32(0), emptyModels.PageSize)
173173
assert.Len(emptyModels.Items, 0)
174+
175+
// Test case 13: Test with excluded models
176+
excludedProvider := testYAMLProviderWithExclusions(t, "testdata/test-list-models-catalog.yaml", []any{
177+
"model-alpha",
178+
})
179+
excludedModels, err := excludedProvider.ListModels(ctx, ListModelsParams{})
180+
if assert.NoError(err) {
181+
assert.NotNil(excludedModels)
182+
assert.Equal(int32(5), excludedModels.Size)
183+
for _, m := range excludedModels.Items {
184+
assert.NotEqual("model-alpha", m.Name)
185+
}
186+
}
174187
}
175188

176189
func testYAMLProvider(t *testing.T, path string) CatalogSourceProvider {
190+
return testYAMLProviderWithExclusions(t, path, nil)
191+
}
192+
193+
func testYAMLProviderWithExclusions(t *testing.T, path string, excludedModels []any) CatalogSourceProvider {
194+
properties := map[string]any{
195+
yamlCatalogPath: path,
196+
}
197+
if excludedModels != nil {
198+
properties["excludedModels"] = excludedModels
199+
}
177200
provider, err := newYamlCatalog(&CatalogSourceConfig{
178-
Properties: map[string]any{
179-
yamlCatalogPath: path,
180-
},
201+
Properties: properties,
181202
})
182203
if err != nil {
183-
t.Fatalf("newYamlCatalog(%s) failed: %v", path, err)
204+
t.Fatalf("newYamlCatalog(%s) with exclusions failed: %v", path, err)
184205
}
185206
return provider
186207
}

manifests/kustomize/options/catalog/README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The `yaml` type sources model metadata from a local YAML file.
3232
##### Properties
3333

3434
- **`yamlCatalogPath`** (*string*, required): The path to the YAML file containing the model definitions. This path is relative to the directory where the `sources.yaml` file is located.
35+
- **`excludedModels`** (*string list*, optional): A list of models to exclude from the catalog. These can be an exact name with a tag (e.g., `model-a:1.0`) or a pattern ending with `*` to exclude all tags for a repository (e.g., `model-b:*`).
3536

3637
##### Example
3738

@@ -43,6 +44,9 @@ catalogs:
4344
enabled: true
4445
properties:
4546
yamlCatalogPath: sample-catalog.yaml
47+
excludedModels:
48+
- model-a:1.0
49+
- model-b:*
4650
```
4751
4852
#### `rhec`
@@ -51,8 +55,8 @@ The `rhec` type sources model metadata from the Red Hat Ecosystem Catalog.
5155

5256
##### Properties
5357

54-
- **`models`** (*list*, required): A list of models to include from the Red Hat Ecosystem Catalog. Each entry in the list must contain a `repository` field.
55-
- **`repository`** (*string*, required): The name of the model repository in the Red Hat Ecosystem Catalog (e.g., `rhelai1/modelcar-granite-7b-starter`).
58+
- **`models`** (*string list*, required): A list of models to include from the Red Hat Ecosystem Catalog. Each entry contains the full name of the model repository in the Red Hat Ecosystem Catalog (e.g., `rhelai1/modelcar-granite-7b-starter`).
59+
- **`excludedModels`** (*string list*, optional): A list of models to exclude from the catalog. These can be an exact name with a tag (e.g., `rhelai1/modelcar-granite-7b-starter:b9514c3`) or a pattern ending with `*` to exclude all tags for a repository (e.g., `rhelai1/modelcar-granite-7b-starter:*`).
5660

5761
##### Example
5862

@@ -64,5 +68,8 @@ catalogs:
6468
enabled: true
6569
properties:
6670
models:
67-
- repository: rhelai1/modelcar-granite-7b-starter
68-
```
71+
- rhelai1/modelcar-granite-7b-starter
72+
excludedModels:
73+
- rhelai1/modelcar-granite-7b-starter:v0
74+
- rhelai1/modelcar-granite-*
75+
```

manifests/kustomize/options/catalog/sources.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,3 @@ catalogs:
55
enabled: true
66
properties:
77
yamlCatalogPath: sample-catalog.yaml
8-
- name: Red Hat Ecosystem Catalog
9-
id: sample_rhec_catalog
10-
type: rhec
11-
enabled: true
12-
properties:
13-
models:
14-
- repository: rhelai1/modelcar-granite-7b-starter
15-

0 commit comments

Comments
 (0)