Skip to content

Commit d131643

Browse files
committed
add graceful handling of non-existent vector stores and custom endpoint configmaps in GetPassthroughEmbeddingProviderInfo/ListExternalVectorStores
1 parent 009e6d5 commit d131643

5 files changed

Lines changed: 97 additions & 1 deletion

File tree

packages/gen-ai/bff/internal/api/aaa_vectorstores_handler_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,35 @@ var _ = Describe("VectorStoresAAHandler", func() {
194194
assert.Contains(t, errorInfo["message"], "namespace parameter is required")
195195
})
196196

197+
It("should return 200 with empty list when the vector stores ConfigMap does not exist", func() {
198+
t := GinkgoT()
199+
rr := httptest.NewRecorder()
200+
201+
// opendatahub namespace exists but has no gen-ai-aa-vector-stores ConfigMap
202+
req, err := http.NewRequest("GET", "/gen-ai/api/v1/aaa/vectorstores?namespace=opendatahub", nil)
203+
require.NoError(t, err)
204+
205+
ctx := context.WithValue(req.Context(), constants.RequestIdentityKey, &integrations.RequestIdentity{
206+
Token: "FAKE_BEARER_TOKEN",
207+
})
208+
ctx = context.WithValue(ctx, constants.NamespaceQueryParameterKey, "opendatahub")
209+
req = req.WithContext(ctx)
210+
211+
app.VectorStoresAAHandler(rr, req, nil)
212+
213+
assert.Equal(t, http.StatusOK, rr.Code)
214+
215+
body, err := io.ReadAll(rr.Result().Body)
216+
require.NoError(t, err)
217+
defer rr.Result().Body.Close()
218+
219+
var response VectorStoresAAEnvelope
220+
err = json.Unmarshal(body, &response)
221+
require.NoError(t, err)
222+
223+
assert.Empty(t, response.Data, "Should return empty list when ConfigMap is absent")
224+
})
225+
197226
It("should return 400 when request identity is missing", func() {
198227
t := GinkgoT()
199228
rr := httptest.NewRecorder()

packages/gen-ai/bff/internal/api/external_vectorstores_handler_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,34 @@ var _ = Describe("ExternalVectorStoresListHandler", func() {
237237
assert.Equal(t, "llama-stack", response.Data.ConfigMapInfo.Namespace)
238238
assert.NotEmpty(t, response.Data.ConfigMapInfo.LastUpdated, "ConfigMap info should have last updated timestamp")
239239
})
240+
241+
It("should return 200 with empty list when the vector stores ConfigMap does not exist", func() {
242+
t := GinkgoT()
243+
rr := httptest.NewRecorder()
244+
245+
// opendatahub namespace exists but has no gen-ai-aa-vector-stores ConfigMap
246+
req, err := http.NewRequest("GET", "/api/v1/vectorstores/external?namespace=opendatahub", nil)
247+
require.NoError(t, err)
248+
249+
ctx := context.WithValue(req.Context(), constants.RequestIdentityKey, &integrations.RequestIdentity{
250+
Token: "FAKE_BEARER_TOKEN",
251+
})
252+
ctx = context.WithValue(ctx, constants.NamespaceQueryParameterKey, "opendatahub")
253+
req = req.WithContext(ctx)
254+
255+
app.ExternalVectorStoresListHandler(rr, req, nil)
256+
257+
assert.Equal(t, http.StatusOK, rr.Code)
258+
259+
body, err := io.ReadAll(rr.Result().Body)
260+
require.NoError(t, err)
261+
defer rr.Result().Body.Close()
262+
263+
var response ExternalVectorStoresListEnvelope
264+
err = json.Unmarshal(body, &response)
265+
require.NoError(t, err)
266+
267+
assert.Empty(t, response.Data.VectorStores, "Should return empty list when ConfigMap is absent")
268+
assert.Equal(t, 0, response.Data.TotalCount, "Should return zero total count when ConfigMap is absent")
269+
})
240270
})

packages/gen-ai/bff/internal/api/lsd_responses_handler_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
"github.com/opendatahub-io/gen-ai/internal/testutil"
3131
"github.com/stretchr/testify/assert"
3232
"github.com/stretchr/testify/require"
33+
apierrors "k8s.io/apimachinery/pkg/api/errors"
34+
"k8s.io/apimachinery/pkg/runtime/schema"
3335
)
3436

3537
var _ = Describe("LlamaStackCreateResponseHandler", func() {
@@ -1631,6 +1633,16 @@ func TestGetPassthroughEmbeddingSecret(t *testing.T) {
16311633
assert.Contains(t, err.Error(), "configmap not found")
16321634
})
16331635

1636+
t.Run("returns no passthrough data when vector stores ConfigMap does not exist", func(t *testing.T) {
1637+
client := &customEndpointMockClient{
1638+
vectorStoresDocErr: apierrors.NewNotFound(schema.GroupResource{Resource: "configmaps"}, "gen-ai-aa-vector-stores"),
1639+
}
1640+
providerData, err := newApp(client).getProviderData(newCtx(), "inf-model", "", "", []string{"vs-1"})
1641+
require.NoError(t, err)
1642+
assert.NotContains(t, providerData, "passthrough_url")
1643+
assert.NotContains(t, providerData, "passthrough_api_key")
1644+
})
1645+
16341646
t.Run("returns error when external models ConfigMap read fails", func(t *testing.T) {
16351647
client := &customEndpointMockClient{
16361648
vectorStoresDoc: vsDoc,
@@ -1641,6 +1653,17 @@ func TestGetPassthroughEmbeddingSecret(t *testing.T) {
16411653
assert.Contains(t, err.Error(), "external models configmap unavailable")
16421654
})
16431655

1656+
t.Run("returns no passthrough data when external models ConfigMap does not exist", func(t *testing.T) {
1657+
client := &customEndpointMockClient{
1658+
vectorStoresDoc: vsDoc,
1659+
externalModelsConfigErr: apierrors.NewNotFound(schema.GroupResource{Resource: "configmaps"}, "gen-ai-aa-custom-model-endpoints"),
1660+
}
1661+
providerData, err := newApp(client).getProviderData(newCtx(), "inf-model", "", "", []string{"vs-1"})
1662+
require.NoError(t, err)
1663+
assert.NotContains(t, providerData, "passthrough_url")
1664+
assert.NotContains(t, providerData, "passthrough_api_key")
1665+
})
1666+
16441667
t.Run("returns error when secret fetch fails", func(t *testing.T) {
16451668
client := &customEndpointMockClient{
16461669
vectorStoresDoc: vsDoc,

packages/gen-ai/bff/internal/repositories/external_models.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/opendatahub-io/gen-ai/internal/integrations/externalmodels"
1414
"github.com/opendatahub-io/gen-ai/internal/integrations/kubernetes"
1515
"github.com/opendatahub-io/gen-ai/internal/models"
16+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1617
)
1718

1819
type ExternalModelsRepository struct{}
@@ -83,7 +84,8 @@ type PassthroughEmbeddingInfo struct {
8384

8485
// GetPassthroughEmbeddingProviderInfo finds the first vector store in vectorStoreIDs that uses
8586
// a custom-endpoint (remote::passthrough) embedding model and returns its URL and API key.
86-
// Returns (nil, nil) when none of the stores use a passthrough embedding model — not an error.
87+
// Returns (nil, nil) if Vector Stores or External Models config not found, or when none of
88+
// the stores use a passthrough embedding model — not an error.
8789
// Returns a non-nil error when ConfigMap or Secret reads fail so callers can fail closed.
8890
func (r *ExternalModelsRepository) GetPassthroughEmbeddingProviderInfo(
8991
client kubernetes.KubernetesClientInterface,
@@ -94,6 +96,9 @@ func (r *ExternalModelsRepository) GetPassthroughEmbeddingProviderInfo(
9496
) (*PassthroughEmbeddingInfo, error) {
9597
vsDoc, err := client.GetVectorStoresConfig(ctx, namespace)
9698
if err != nil {
99+
if apierrors.IsNotFound(err) {
100+
return nil, nil
101+
}
97102
return nil, fmt.Errorf("failed to get vector stores config: %w", err)
98103
}
99104

@@ -104,6 +109,9 @@ func (r *ExternalModelsRepository) GetPassthroughEmbeddingProviderInfo(
104109

105110
externalModelsConfig, err := client.GetExternalModelsConfig(ctx, namespace)
106111
if err != nil {
112+
if apierrors.IsNotFound(err) {
113+
return nil, nil
114+
}
107115
return nil, fmt.Errorf("failed to get external models config: %w", err)
108116
}
109117

packages/gen-ai/bff/internal/repositories/external_vectorstores.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
kubernetes "github.com/opendatahub-io/gen-ai/internal/integrations/kubernetes"
1111
"github.com/opendatahub-io/gen-ai/internal/models"
1212
"gopkg.in/yaml.v2"
13+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1314
)
1415

1516
// ExternalVectorStoresRepository handles external vector store operations
@@ -35,6 +36,8 @@ type ExternalVectorStoresResult struct {
3536
// parses the providers and registered_resources sections, and resolves provider metadata per store.
3637
// Embedding model status (not_available / available / registered) is computed client-side by the
3738
// frontend using the already-loaded merged models data.
39+
// Returns an empty result (not an error) when the ConfigMap does not exist — a fresh namespace
40+
// with no external vector stores configured is a valid empty state.
3841
func (r *ExternalVectorStoresRepository) ListExternalVectorStores(
3942
ctx context.Context,
4043
k8sClient kubernetes.KubernetesClientInterface,
@@ -43,6 +46,9 @@ func (r *ExternalVectorStoresRepository) ListExternalVectorStores(
4346
) (*ExternalVectorStoresResult, error) {
4447
configMap, err := k8sClient.GetConfigMap(ctx, identity, namespace, constants.VectorStoresConfigMapName)
4548
if err != nil {
49+
if apierrors.IsNotFound(err) {
50+
return &ExternalVectorStoresResult{VectorStores: []models.ExternalVectorStoreSummary{}}, nil
51+
}
4652
return nil, fmt.Errorf("failed to get vector stores ConfigMap: %w", err)
4753
}
4854

0 commit comments

Comments
 (0)