Skip to content

Commit 413fa0a

Browse files
Merge branch 'main' of github.com:redhat-et/maas-billing into clean_maas
2 parents a50702b + ded9da5 commit 413fa0a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+9996
-346
lines changed

.github/hack/install-odh.sh

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
# Prerequisites: cert-manager and LWS operators (run install-cert-manager-and-lws.sh first).
66
#
77
# Environment variables:
8-
# OPERATOR_CATALOG - Custom catalog image (optional). When unset, uses community-operators (ODH 3.3).
8+
# OPERATOR_CATALOG - Custom catalog image (optional). When unset, uses community-operators.
99
# Set to e.g. quay.io/opendatahub/opendatahub-operator-catalog:latest for custom builds.
10-
# OPERATOR_CHANNEL - Subscription channel (default: fast-3 for community, fast for custom catalog)
10+
# OPERATOR_CHANNEL - Subscription channel (default: fast-3)
11+
# OPERATOR_STARTING_CSV - Pin Subscription startingCSV (default: opendatahub-operator.v3.4.0-ea.1). Set to "-" to omit.
12+
# OPERATOR_INSTALL_PLAN_APPROVAL - Manual (default) or Automatic; use "-" to omit.
13+
# Manual: blocks auto-upgrades; this script auto-approves only the first InstallPlan so install does not stall.
1114
# OPERATOR_IMAGE - Custom operator image to patch into CSV (optional)
1215
#
1316
# Usage: ./install-odh.sh
@@ -21,6 +24,8 @@ DATA_DIR="${REPO_ROOT}/scripts/data"
2124
NAMESPACE="${OPERATOR_NAMESPACE:-opendatahub}"
2225
OPERATOR_CATALOG="${OPERATOR_CATALOG:-}"
2326
OPERATOR_CHANNEL="${OPERATOR_CHANNEL:-}"
27+
OPERATOR_STARTING_CSV="${OPERATOR_STARTING_CSV:-}"
28+
OPERATOR_INSTALL_PLAN_APPROVAL="${OPERATOR_INSTALL_PLAN_APPROVAL:-}"
2429
OPERATOR_IMAGE="${OPERATOR_IMAGE:-}"
2530

2631
# Source deployment helpers
@@ -59,28 +64,38 @@ patch_operator_csv_if_needed() {
5964
echo "=== Installing OpenDataHub operator ==="
6065
echo ""
6166

62-
# 1. Catalog setup: use community-operators (ODH 3.3) by default, or custom catalog when OPERATOR_CATALOG is set
67+
# 1. Catalog setup: community-operators by default, or custom catalog when OPERATOR_CATALOG is set
6368
echo "1. Setting up ODH catalog..."
6469
if [[ -n "$OPERATOR_CATALOG" ]]; then
6570
echo " Using custom catalog: $OPERATOR_CATALOG"
6671
create_custom_catalogsource "odh-custom-catalog" "openshift-marketplace" "$OPERATOR_CATALOG"
6772
catalog_source="odh-custom-catalog"
68-
channel="${OPERATOR_CHANNEL:-fast}"
73+
channel="${OPERATOR_CHANNEL:-fast-3}"
6974
else
70-
echo " Using community-operators (ODH 3.3)"
75+
echo " Using community-operators"
7176
catalog_source="community-operators"
7277
channel="${OPERATOR_CHANNEL:-fast-3}"
7378
fi
7479

80+
# Pin to ODH 3.4 EA1 unless overridden (omit with OPERATOR_STARTING_CSV=- to follow channel head)
81+
starting_csv="${OPERATOR_STARTING_CSV:-opendatahub-operator.v3.4.0-ea.1}"
82+
[[ "$starting_csv" == "-" ]] && starting_csv=""
83+
84+
# Manual = no auto-upgrades; install_olm_operator still approves the first InstallPlan programmatically
85+
plan_approval="${OPERATOR_INSTALL_PLAN_APPROVAL:-Manual}"
86+
[[ "$plan_approval" == "-" ]] && plan_approval=""
87+
7588
# 2. Install ODH operator via OLM
7689
echo "2. Installing ODH operator..."
7790
install_olm_operator \
7891
"opendatahub-operator" \
7992
"$NAMESPACE" \
8093
"$catalog_source" \
8194
"$channel" \
82-
"" \
83-
"AllNamespaces"
95+
"$starting_csv" \
96+
"AllNamespaces" \
97+
"openshift-marketplace" \
98+
"$plan_approval"
8499

85100
# 3. Patch CSV with custom image if specified
86101
if [[ -n "$OPERATOR_IMAGE" ]]; then

.gitleaks.toml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Gitleaks configuration for opendatahub-io repos
2+
# Synced from security-config. Do not edit in target repos.
3+
#
4+
# Path allowlists use Go regex syntax.
5+
# Real credentials should NEVER be committed to any repository.
6+
7+
[extend]
8+
useDefault = true
9+
10+
[allowlist]
11+
description = "Exclude test fixtures, mock data, sample configs, and CI resources"
12+
paths = [
13+
# Go test files (commonly contain mock credentials)
14+
'''.*_test\.go$''',
15+
16+
# JS/TS test files (.spec.ts, .test.tsx, etc.)
17+
'''.*\.spec\.(ts|tsx|js|jsx)$''',
18+
'''.*\.test\.(ts|tsx|js|jsx)$''',
19+
20+
# JS/TS test directories
21+
'''__tests__/''',
22+
23+
# Go testdata directories
24+
'''testdata/''',
25+
26+
# Python test data directories
27+
'''test_data/''',
28+
29+
# Test fixtures
30+
'''fixtures/''',
31+
32+
# JavaScript/TypeScript mocks
33+
'''__mocks__/''',
34+
35+
# Go/Java/TS mock directories
36+
'''mocks/''',
37+
'''k8mocks/''',
38+
39+
# Sample and example configs with placeholder credentials
40+
'''docs/samples/''',
41+
'''config/samples/''',
42+
'''config/overlays/test/''',
43+
44+
# CI/GitHub Actions test resources
45+
'''\.github/resources/''',
46+
47+
# E2E test credentials
48+
'''test/e2e/credentials/''',
49+
'''tests/e2e/credentials/''',
50+
51+
# OpenShift CI sample resources
52+
'''openshift-ci/resources/samples/''',
53+
54+
# Cypress test data
55+
'''cypress/fixtures/''',
56+
'''cypress/tests/mocked/''',
57+
58+
# Test certificate and key files
59+
'''tests/data/.*\.(pem|crt|key)$''',
60+
]
61+
62+
# Known test/placeholder credentials used in documentation and tests
63+
regexes = [
64+
'''database-password\s*:\s*"?(The)?BlurstOfTimes"?''',
65+
'''database-user\s*:\s*"?mlmduser"?''',
66+
'''database-user\s*:\s*"?modelregistryuser"?''',
67+
]

.gitleaksignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Gitleaks ignore file
2+
# Add false positive fingerprints below (one per line)
3+
# Format: commit:file:rule-id:line or file:rule-id:line
4+
#
5+
# For path-based exclusions, use .gitleaks.toml allowlist instead.

.tekton/odh-maas-api-pull-request.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ spec:
2929
value: maas-api/Dockerfile
3030
- name: path-context
3131
value: maas-api
32+
- name: build-platforms
33+
value:
34+
- linux/x86_64
35+
- linux/arm64
36+
- linux/ppc64le
37+
- linux/s390x
3238
- name: additional-tags
3339
value:
3440
- 'odh-pr-{{revision}}'

.tekton/odh-maas-api-push.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ spec:
2828
value: maas-api/Dockerfile
2929
- name: path-context
3030
value: maas-api
31+
- name: build-platforms
32+
value:
33+
- linux/x86_64
34+
- linux/arm64
35+
- linux/ppc64le
36+
- linux/s390x
3137
pipelineRef:
3238
resolver: git
3339
params:

.tekton/odh-maas-controller-pull-request.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ spec:
3434
- 'odh-pr-{{revision}}'
3535
- name: pipeline-type
3636
value: pull-request
37+
- name: build-platforms
38+
value:
39+
- linux/x86_64
40+
- linux/arm64
41+
- linux/ppc64le
42+
- linux/s390x
3743
pipelineRef:
3844
resolver: git
3945
params:

.tekton/odh-maas-controller-push.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ spec:
2828
value: Dockerfile
2929
- name: path-context
3030
value: maas-controller
31+
- name: build-platforms
32+
value:
33+
- linux/x86_64
34+
- linux/arm64
35+
- linux/ppc64le
36+
- linux/s390x
3137
pipelineRef:
3238
resolver: git
3339
params:

OWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ approvers:
44
- chaitanya1731
55
- nerdalert
66
- jland-redhat
7+
- nirrozenbaum
78
- dmytro-zaharnytskyi
89
- SB159
910
- noyitz
@@ -21,6 +22,7 @@ reviewers:
2122
- chaitanya1731
2223
- nerdalert
2324
- jland-redhat
25+
- nirrozenbaum
2426
- dmytro-zaharnytskyi
2527
- SB159
2628
- noyitz

deployment/base/maas-controller/crd/bases/maas.opendatahub.io_maasmodelrefs.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ spec:
5555
spec:
5656
description: MaaSModelSpec defines the desired state of MaaSModelRef
5757
properties:
58+
credentialRef:
59+
description: |-
60+
CredentialRef references a Kubernetes Secret containing the provider API key.
61+
The Secret must contain a data key "api-key" with the credential value.
62+
Only used when modelRef.kind=ExternalModel.
63+
properties:
64+
name:
65+
description: Name is the name of the Secret
66+
maxLength: 253
67+
minLength: 1
68+
type: string
69+
namespace:
70+
description: Namespace is the namespace of the Secret. Defaults
71+
to the MaaSModelRef namespace if omitted.
72+
maxLength: 253
73+
type: string
74+
required:
75+
- name
76+
type: object
5877
endpointOverride:
5978
description: |-
6079
EndpointOverride, when set, overrides the endpoint URL that the controller
@@ -64,6 +83,16 @@ spec:
6483
modelRef:
6584
description: ModelRef references the actual model endpoint
6685
properties:
86+
endpoint:
87+
description: |-
88+
Endpoint is the FQDN of the external provider (no scheme or path).
89+
e.g. "api.openai.com". Only used when kind=ExternalModel.
90+
This field is metadata for downstream consumers (e.g. BBR provider-resolver plugin)
91+
and is not used by the controller for endpoint derivation. Use spec.endpointOverride
92+
to override the controller-derived endpoint.
93+
maxLength: 253
94+
pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$
95+
type: string
6796
kind:
6897
description: Kind determines which fields are available
6998
enum:
@@ -73,10 +102,20 @@ spec:
73102
name:
74103
description: Name is the name of the model resource
75104
type: string
105+
provider:
106+
description: |-
107+
Provider identifies the API format and auth type for external models.
108+
e.g. "openai", "anthropic". Only used when kind=ExternalModel.
109+
maxLength: 63
110+
type: string
76111
required:
77112
- kind
78113
- name
79114
type: object
115+
x-kubernetes-validations:
116+
- message: provider is required when kind is ExternalModel
117+
rule: self.kind != 'ExternalModel' || has(self.provider) && self.provider
118+
!= ''
80119
required:
81120
- modelRef
82121
type: object

maas-api/internal/handlers/models.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ func (h *ModelsHandler) selectSubscriptionsForListing(
7878

7979
// Single subscription selection (existing behavior)
8080
if h.subscriptionSelector != nil {
81-
result, err := h.subscriptionSelector.Select(userContext.Groups, userContext.Username, requestedSubscription)
81+
//nolint:unqueryvet,nolintlint // Select is a method, not a SQL query
82+
result, err := h.subscriptionSelector.Select(userContext.Groups, userContext.Username, requestedSubscription, "")
8283
if err != nil {
8384
h.handleSubscriptionSelectionError(c, err)
8485
return nil, true
@@ -99,6 +100,7 @@ func (h *ModelsHandler) selectSubscriptionsForListing(
99100
// handleSubscriptionSelectionError handles errors from subscription selection and sends appropriate HTTP responses.
100101
func (h *ModelsHandler) handleSubscriptionSelectionError(c *gin.Context, err error) {
101102
var multipleSubsErr *subscription.MultipleSubscriptionsError
103+
var ambiguousErr *subscription.SubscriptionAmbiguousError
102104
var accessDeniedErr *subscription.AccessDeniedError
103105
var notFoundErr *subscription.SubscriptionNotFoundError
104106
var noSubErr *subscription.NoSubscriptionError
@@ -117,6 +119,16 @@ func (h *ModelsHandler) handleSubscriptionSelectionError(c *gin.Context, err err
117119
return
118120
}
119121

122+
if errors.As(err, &ambiguousErr) {
123+
h.logger.Debug("Subscription name is ambiguous")
124+
c.JSON(http.StatusForbidden, gin.H{
125+
"error": gin.H{
126+
"message": err.Error(),
127+
"type": "permission_error",
128+
}})
129+
return
130+
}
131+
120132
if errors.As(err, &accessDeniedErr) {
121133
h.logger.Debug("Access denied to subscription")
122134
c.JSON(http.StatusForbidden, gin.H{
@@ -255,7 +267,7 @@ func (h *ModelsHandler) ListLLMs(c *gin.Context) {
255267
} else {
256268
// User has zero accessible subscriptions - return empty list
257269
h.logger.Debug("User has zero accessible subscriptions, returning empty model list")
258-
// modelList is already initialized to empty slice at line 235
270+
// modelList is already initialized to empty slice above
259271
}
260272
} else {
261273
// Filter models by subscription(s) and aggregate subscriptions
@@ -268,8 +280,22 @@ func (h *ModelsHandler) ListLLMs(c *gin.Context) {
268280
modelsByKey := make(map[modelKey]*models.Model)
269281

270282
for _, sub := range subscriptionsToUse {
271-
h.logger.Debug("Filtering models by subscription", "subscription", sub.Name, "modelCount", len(list))
272-
filteredModels := h.modelMgr.FilterModelsByAccess(c.Request.Context(), list, authHeader, sub.Name)
283+
// Pre-filter by modelRefs if available (optimization to reduce HTTP calls)
284+
modelsToCheck := list
285+
if len(sub.ModelRefs) > 0 {
286+
h.logger.Debug("Pre-filtering models by subscription modelRefs",
287+
"subscription", sub.Name,
288+
"totalModels", len(list),
289+
"modelRefsCount", len(sub.ModelRefs),
290+
)
291+
modelsToCheck = filterModelsBySubscription(list, sub.ModelRefs)
292+
h.logger.Debug("After modelRef filtering", "modelsToCheck", len(modelsToCheck))
293+
}
294+
295+
// Use qualified "namespace/name" format for accurate authorization checks
296+
qualifiedSubName := sub.Namespace + "/" + sub.Name
297+
h.logger.Debug("Filtering models by subscription", "subscription", qualifiedSubName, "modelCount", len(modelsToCheck))
298+
filteredModels := h.modelMgr.FilterModelsByAccess(c.Request.Context(), modelsToCheck, authHeader, qualifiedSubName)
273299

274300
for _, model := range filteredModels {
275301
subInfo := models.SubscriptionInfo{
@@ -323,3 +349,29 @@ func (h *ModelsHandler) ListLLMs(c *gin.Context) {
323349
Data: modelList,
324350
})
325351
}
352+
353+
// filterModelsBySubscription filters models to only those matching the subscription's modelRefs.
354+
func filterModelsBySubscription(modelList []models.Model, modelRefs []subscription.ModelRefInfo) []models.Model {
355+
if len(modelRefs) == 0 {
356+
return modelList
357+
}
358+
359+
// Build map of allowed models for fast lookup
360+
allowed := make(map[string]bool)
361+
for _, ref := range modelRefs {
362+
key := ref.Namespace + "/" + ref.Name
363+
allowed[key] = true
364+
}
365+
366+
// Filter models
367+
filtered := make([]models.Model, 0, len(modelList))
368+
for _, model := range modelList {
369+
// Models from MaaSModelRefLister have OwnedBy set to namespace
370+
modelKey := model.OwnedBy + "/" + model.ID
371+
if allowed[modelKey] {
372+
filtered = append(filtered, model)
373+
}
374+
}
375+
376+
return filtered
377+
}

0 commit comments

Comments
 (0)