Skip to content

Commit c3934ce

Browse files
authored
Merge pull request #400 from hbelmiro/main-to-stable
Main to stable
2 parents 57ca8ca + a8e29f5 commit c3934ce

45 files changed

Lines changed: 4141 additions & 1397 deletions

Some content is hidden

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

.github/resources/scripts/configure-mlflow.sh

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ KFP_NAMESPACE="${1:?KFP namespace required}"
2929
MLFLOW_NAMESPACE="${2:?MLflow namespace required}"
3030
CONFIG_JSON_PATH="${3:?Path to source config.json required}"
3131

32+
MLFLOW_CA_CONFIGMAP="mlflow-ca-cert"
33+
KFP_CA_BUNDLE_DIR="/kfp/certs"
34+
KFP_CA_BUNDLE_PATH="${KFP_CA_BUNDLE_DIR}/ca.crt"
35+
3236
echo "Services in ${MLFLOW_NAMESPACE} namespace:"
3337
kubectl get svc -n "$MLFLOW_NAMESPACE" --no-headers
3438
MLFLOW_SVC=$(kubectl get svc -n "$MLFLOW_NAMESPACE" --no-headers -o custom-columns=":metadata.name" | grep -i mlflow | head -1)
@@ -42,9 +46,41 @@ MLFLOW_STATIC_PREFIX="/mlflow"
4246
MLFLOW_ENDPOINT="https://${MLFLOW_HOST}:${MLFLOW_PORT}${MLFLOW_STATIC_PREFIX}"
4347
echo "MLflow service: $MLFLOW_SVC port=$MLFLOW_PORT endpoint=$MLFLOW_ENDPOINT"
4448

45-
MLFLOW_PATCH=$(jq -n --arg endpoint "$MLFLOW_ENDPOINT" '{
49+
# --- Extract CA certificate from the MLflow TLS secret ---
50+
CA_CERT_FILE="/tmp/mlflow-ca.crt"
51+
52+
echo "Extracting MLflow CA certificate..."
53+
TLS_SECRET=$(kubectl get secret -n "$MLFLOW_NAMESPACE" -o custom-columns=":metadata.name" --no-headers | grep -i "mlflow.*tls\|tls.*mlflow" | head -1)
54+
if [ -z "$TLS_SECRET" ]; then
55+
TLS_SECRET=$(kubectl get secret -n "$MLFLOW_NAMESPACE" --field-selector type=kubernetes.io/tls -o custom-columns=":metadata.name" --no-headers | head -1)
56+
fi
57+
if [ -z "$TLS_SECRET" ]; then
58+
echo "ERROR: No TLS secret found in namespace $MLFLOW_NAMESPACE"
59+
exit 1
60+
fi
61+
echo "Found TLS secret: $TLS_SECRET"
62+
63+
# cert-manager stores the CA in the ca.crt key; fall back to tls.crt if ca.crt is absent
64+
CA_DATA=$(kubectl get secret -n "$MLFLOW_NAMESPACE" "$TLS_SECRET" -o jsonpath='{.data.ca\.crt}' 2>/dev/null)
65+
if [ -z "$CA_DATA" ]; then
66+
echo "ca.crt not found in secret, falling back to tls.crt"
67+
CA_DATA=$(kubectl get secret -n "$MLFLOW_NAMESPACE" "$TLS_SECRET" -o jsonpath='{.data.tls\.crt}')
68+
fi
69+
if [ -z "$CA_DATA" ]; then
70+
echo "ERROR: Could not extract CA certificate from secret $TLS_SECRET"
71+
exit 1
72+
fi
73+
echo "$CA_DATA" | base64 -d > "$CA_CERT_FILE"
74+
echo "CA certificate extracted to $CA_CERT_FILE ($(wc -l < "$CA_CERT_FILE") lines)"
75+
76+
# ConfigMap consumed by the API server (plugin config) and compiler (driver/launcher pods).
77+
kubectl create configmap "$MLFLOW_CA_CONFIGMAP" -n "$KFP_NAMESPACE" \
78+
--from-file=ca.crt="$CA_CERT_FILE" --dry-run=client -o yaml | kubectl apply -f -
79+
80+
# --- Build the MLflow plugin config with caBundlePath ---
81+
MLFLOW_PATCH=$(jq -n --arg endpoint "$MLFLOW_ENDPOINT" --arg caBundlePath "$KFP_CA_BUNDLE_PATH" '{
4682
endpoint: $endpoint,
47-
tls: { insecureSkipVerify: true },
83+
tls: { caBundlePath: $caBundlePath },
4884
settings: { workspacesEnabled: true }
4985
}')
5086

@@ -54,10 +90,11 @@ jq --argjson mlflow "$MLFLOW_PATCH" '. + { plugins: { mlflow: $mlflow } }' \
5490
echo "Patched config.json plugins.mlflow:"
5591
jq '.plugins.mlflow' /tmp/kfp-config.json
5692

93+
# --- Deploy plugin config and wire CA trust on the API server + compiler ---
5794
kubectl create configmap kfp-mlflow-config -n "$KFP_NAMESPACE" \
5895
--from-file=config.json=/tmp/kfp-config.json --dry-run=client -o yaml | kubectl apply -f -
5996
kubectl patch deployment ml-pipeline -n "$KFP_NAMESPACE" --type=strategic -p \
60-
'{"spec":{"template":{"spec":{"volumes":[{"name":"mlflow-cfg","configMap":{"name":"kfp-mlflow-config"}}],"containers":[{"name":"ml-pipeline-api-server","volumeMounts":[{"name":"mlflow-cfg","mountPath":"/config/config.json","subPath":"config.json"}]}]}}}}'
97+
'{"spec":{"template":{"spec":{"volumes":[{"name":"mlflow-cfg","configMap":{"name":"kfp-mlflow-config"}},{"name":"mlflow-ca","configMap":{"name":"'"$MLFLOW_CA_CONFIGMAP"'"}}],"containers":[{"name":"ml-pipeline-api-server","env":[{"name":"CABUNDLE_CONFIGMAP_NAME","value":"'"$MLFLOW_CA_CONFIGMAP"'"}],"volumeMounts":[{"name":"mlflow-cfg","mountPath":"/config/config.json","subPath":"config.json"},{"name":"mlflow-ca","mountPath":"'"$KFP_CA_BUNDLE_DIR"'","readOnly":true}]}]}}}}'
6198
kubectl rollout status deployment/ml-pipeline -n "$KFP_NAMESPACE" --timeout=180s
6299

63100
pkill -f "kubectl port-forward.*ml-pipeline.*8888" || true
@@ -87,12 +124,13 @@ if [ -n "${GITHUB_ENV:-}" ]; then
87124
echo "MLFLOW_PORT_FORWARD_NS=$MLFLOW_NAMESPACE" >> "$GITHUB_ENV"
88125
echo "MLFLOW_PORT_FORWARD_SVC=$MLFLOW_SVC" >> "$GITHUB_ENV"
89126
echo "MLFLOW_PORT_FORWARD_REMOTE_PORT=$MLFLOW_PORT" >> "$GITHUB_ENV"
127+
echo "MLFLOW_CA_BUNDLE_PATH=$CA_CERT_FILE" >> "$GITHUB_ENV"
90128
if [ -n "$SA_TOKEN" ]; then
91129
echo "MLFLOW_BEARER_TOKEN=$SA_TOKEN" >> "$GITHUB_ENV"
92-
echo "Exported MLFLOW_BEARER_TOKEN and MLFLOW_WORKSPACE for test helpers"
130+
echo "Exported MLFLOW_BEARER_TOKEN, MLFLOW_WORKSPACE, and MLFLOW_CA_BUNDLE_PATH for test helpers"
93131
else
94132
echo "WARNING: Could not create SA token; MLflow requests may be unauthenticated"
95-
echo "Exported MLFLOW_WORKSPACE only"
133+
echo "Exported MLFLOW_WORKSPACE and MLFLOW_CA_BUNDLE_PATH only"
96134
fi
97135
fi
98136

@@ -105,7 +143,7 @@ CURL_HEADERS=(-H "X-MLflow-Workspace: $KFP_NAMESPACE")
105143

106144
STATUS=000
107145
for i in $(seq 1 30); do
108-
STATUS=$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 10 \
146+
STATUS=$(curl -s --cacert "$CA_CERT_FILE" -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 10 \
109147
"${CURL_HEADERS[@]}" "$HEALTH_URL" 2>/dev/null || echo "000")
110148
if [ "$STATUS" != "000" ] && [ "$STATUS" -lt 500 ] 2>/dev/null; then
111149
echo "MLflow backend is healthy on localhost:8080 (HTTPS, status=$STATUS)"

.github/workflows/e2e-test.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ env:
1212
AWS_SECRET_ACCESS_KEY: 'minio123'
1313
AWS_S3_BUCKET: 'mlpipeline'
1414
MLFLOW_TRACKING_URI: "https://localhost:8080/mlflow"
15-
MLFLOW_TRACKING_INSECURE_TLS: "true"
1615
on:
1716
push:
1817
branches:
@@ -357,12 +356,12 @@ jobs:
357356

358357
- name: Deploy MLflow
359358
id: deploy-mlflow
360-
uses: opendatahub-io/mlflow-operator/.github/actions/deploy@b55bcb6aa528ccecf8904745bb0218350b1ff453
359+
uses: opendatahub-io/mlflow-operator/.github/actions/deploy@ec59b7c996ef4d14c448254132e21bd85fd1c28f
361360
if: ${{ steps.create-cluster.outcome == 'success' }}
362361
with:
363362
namespace: 'opendatahub'
364-
mlflow_image: quay.io/${{ github.repository_owner == 'red-hat-data-services' && 'rhoai' || 'opendatahub' }}/mlflow:odh-stable
365-
mlflow_operator_image: quay.io/${{ github.repository_owner == 'red-hat-data-services' && 'rhoai' || 'opendatahub' }}/mlflow-operator:odh-stable
363+
mlflow_image: quay.io/${{ github.repository_owner == 'red-hat-data-services' && 'rhoai' || 'opendatahub' }}/mlflow:odh-stable-490cd77b5b697a8ad0c5a84272983f668b961524
364+
mlflow_operator_image: quay.io/${{ github.repository_owner == 'red-hat-data-services' && 'rhoai' || 'opendatahub' }}/mlflow-operator:ec59b7c996ef4d14c448254132e21bd85fd1c28f
366365
backend_store: ${{ matrix.backend_store }}
367366
artifact_storage: ${{ matrix.artifact_storage }}
368367
registry_store: ${{ matrix.registry_store }}

.github/workflows/upgrade-test.yml

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,47 @@ jobs:
122122
skip_operator_deployment: ${{ env.SKIP_OPERATOR_DEPLOYMENT }}
123123
pod_to_pod_tls_enabled: ${{ matrix.tls_enabled }}
124124

125+
- name: Wait for upgrade rollout and re-establish port-forwarding
126+
if: ${{ steps.deploy.outcome == 'success' }}
127+
id: post-upgrade-port-forward
128+
shell: bash
129+
run: |
130+
DEPLOYMENT_NAME="${{ steps.deploy.outputs.deployment_name }}"
131+
NAMESPACE="${{ env.NAMESPACE }}"
132+
133+
OLD_POD_UID=$(kubectl get pod -l app=$DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.items[0].metadata.uid}')
134+
echo "Current pod UID: $OLD_POD_UID"
135+
136+
echo "Waiting for operator to roll the deployment..."
137+
for i in $(seq 1 36); do
138+
NEW_POD_UID=$(kubectl get pod -l app=$DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.items[0].metadata.uid}' 2>/dev/null || echo "")
139+
if [ -n "$NEW_POD_UID" ] && [ "$NEW_POD_UID" != "$OLD_POD_UID" ]; then
140+
echo "New pod detected (UID: $NEW_POD_UID) after $((i * 5))s"
141+
break
142+
fi
143+
if [ $i -eq 36 ]; then
144+
echo "ERROR: Pod UID did not change after 180s — operator did not roll the deployment"
145+
exit 1
146+
fi
147+
sleep 5
148+
done
149+
150+
echo "Waiting for rollout to complete..."
151+
kubectl rollout status deployment/$DEPLOYMENT_NAME -n $NAMESPACE --timeout=300s
152+
153+
echo "Waiting for pod to be ready..."
154+
kubectl wait --for=condition=ready pod -l app=$DEPLOYMENT_NAME -n $NAMESPACE --timeout=120s --field-selector=status.phase=Running
155+
156+
echo "Killing stale port-forward processes..."
157+
pkill -f "kubectl port-forward.*8888" || true
158+
sleep 3
159+
160+
echo "Re-establishing port-forward..."
161+
./.github/resources/scripts/forward-port.sh "$NAMESPACE" "$DEPLOYMENT_NAME" 8888 8888
162+
125163
- name: Verify Upgrade
126164
uses: ./.github/actions/test-and-report
127-
if: ${{ steps.deploy.outcome == 'success' }}
165+
if: ${{ steps.post-upgrade-port-forward.outcome == 'success' }}
128166
with:
129167
test_directory: ${{ env.TESTS_DIR }}
130168
test_label: "UpgradeVerification"

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ ginkgo -v --label-filter="Smoke" ./backend/test/v2/api
243243
ginkgo -v ./backend/test/end2end -- -namespace=kubeflow -isDebugMode=true
244244
```
245245

246-
Without `--label-filter`, `gpu`-labeled E2E tests run as well (they request accelerators) and may pend or fail on CPU-only clusters; pass `--label-filter` to limit what runs (for example `Smoke`, or `gpu` on GPU-capable clusters). NVIDIA vs AMD fixtures: set `KFP_E2E_GPU_VENDOR` to `nvidia` (default if unset), `amd`, or `both` so only matching IRs run (`pytorch_nvidia_gpu_availability.yaml` / `pytorch_amd_gpu_availability.yaml`). Any value other than `amd`/`both`/`all` falls back to `nvidia`.
246+
Without `--label-filter`, `gpu`-labeled E2E tests run as well (they request accelerators) and may pend or fail on CPU-only clusters; pass `--label-filter` to limit what runs (for example `Smoke`, or `gpu` on GPU-capable clusters). NVIDIA vs AMD fixtures: pass `-gpuVendor` flag to `nvidia` (default), `amd`, or `both` so only matching IRs run (`pytorch_nvidia_gpu_availability.yaml` / `pytorch_amd_gpu_availability.yaml`). Any value other than `amd`/`both`/`all` falls back to `nvidia`.
247247

248248
Test data is centralized under:
249249

backend/src/apiserver/config/config.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ func LoadSamples(resourceManager *resource.ResourceManager, sampleConfigPath str
261261
}
262262

263263
// Create pipeline if it does not already exist
264-
p, fetchErr := resourceManager.GetPipelineByNameAndNamespace(cfg.Name, common.GetPodNamespace())
264+
namespace := resourceManager.ReplaceNamespace(common.GetPodNamespace())
265+
p, fetchErr := resourceManager.GetPipelineByNameAndNamespace(cfg.Name, namespace)
265266
if fetchErr != nil {
266267
if util.IsUserErrorCodeMatch(fetchErr, codes.NotFound) {
267268
p, configErr = resourceManager.CreatePipeline(&model.Pipeline{
@@ -271,12 +272,21 @@ func LoadSamples(resourceManager *resource.ResourceManager, sampleConfigPath str
271272
Tags: tags,
272273
})
273274
if configErr != nil {
274-
// Log the error but not fail. The API Server pod can restart and it could potentially cause
275-
// name collision. In the future, we might consider loading samples during deployment, instead
276-
// of when API server starts.
277-
glog.Warningf(fmt.Sprintf(
278-
"Failed to create pipeline for %s. Error: %v", cfg.Name, configErr))
279-
continue
275+
if util.IsUserErrorCodeMatch(configErr, codes.AlreadyExists) {
276+
// Retry without namespace filter. In standalone mode this
277+
// finds the empty-namespace pipeline; safe because sample
278+
// names are deterministic and collisions across namespaces
279+
// are not expected for managed pipelines.
280+
p, fetchErr = resourceManager.GetPipelineByNameAndNamespace(cfg.Name, "")
281+
if fetchErr != nil {
282+
glog.Warningf("Failed to create or find pipeline %s: create=%v, fetch=%v", cfg.Name, configErr, fetchErr)
283+
continue
284+
}
285+
glog.Infof("Pipeline %s already exists (id=%s), proceeding to version check.", cfg.Name, p.UUID)
286+
} else {
287+
glog.Warningf("Failed to create pipeline for %s. Error: %v", cfg.Name, configErr)
288+
continue
289+
}
280290
} else {
281291
glog.Info(fmt.Sprintf("Successfully uploaded Pipeline %s.", cfg.Name))
282292
}
@@ -311,7 +321,7 @@ func LoadSamples(resourceManager *resource.ResourceManager, sampleConfigPath str
311321
// If the Pipeline Version exists, do nothing
312322
// Otherwise upload new Pipeline Version for
313323
// this pipeline.
314-
_, fetchErr = resourceManager.GetPipelineVersionByName(pvName)
324+
_, fetchErr = resourceManager.GetPipelineVersionByName(p.UUID, pvName)
315325
if fetchErr != nil {
316326
if util.IsUserErrorCodeMatch(fetchErr, codes.NotFound) {
317327
_, configErr = resourceManager.CreatePipelineVersion(

backend/src/apiserver/config/config.json

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,5 @@
2424
"CacheEnabled": "true",
2525
"CRON_SCHEDULE_TIMEZONE": "UTC",
2626
"CACHE_IMAGE": "ghcr.io/containerd/busybox",
27-
"CACHE_NODE_RESTRICTIONS": "false",
28-
"plugins": {
29-
"mlflow": {
30-
"endpoint": "https://mlflow.opendatahub.svc.cluster.local:8443",
31-
"timeout": "60s",
32-
"tls": {
33-
"insecureSkipVerify": false,
34-
"caBundlePath": "/etc/mlflow-tracking-ca/ca.crt"
35-
},
36-
"settings": {
37-
"workspacesEnabled": true
38-
}
39-
}
40-
}
27+
"CACHE_NODE_RESTRICTIONS": "false"
4128
}

0 commit comments

Comments
 (0)