Skip to content

Commit c3b2141

Browse files
committed
add test for mlflow
1 parent 2d97710 commit c3b2141

File tree

4 files changed

+238
-7
lines changed

4 files changed

+238
-7
lines changed

.github/workflows/mlflow-ci.yml

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ on:
55
paths:
66
- 'applications/mlflow/charts/**'
77
- 'applications/mlflow/kots/**'
8+
- 'applications/mlflow/tests/**'
89
- '.github/workflows/mlflow-ci.yml'
910
push:
1011
branches:
1112
- main
1213
paths:
1314
- 'applications/mlflow/charts/**'
1415
- 'applications/mlflow/kots/**'
16+
- 'applications/mlflow/tests/**'
1517
- '.github/workflows/mlflow-ci.yml'
1618

1719
env:
@@ -148,6 +150,10 @@ jobs:
148150
# version: 1.31
149151
#- distribution: kind
150152
# version: 1.30
153+
config:
154+
- name: nodeport-ingress-disabled
155+
values_file: tests/helm/nodeport-ingress-disabled.yaml
156+
port: 30080
151157
steps:
152158
- name: Checkout
153159
uses: actions/checkout@v4
@@ -205,10 +211,21 @@ jobs:
205211
api-token: ${{ secrets.REPLICATED_PLATFORM_EXAMPLES_TOKEN }}
206212
kubernetes-distribution: ${{ matrix.cluster.distribution }}
207213
kubernetes-version: ${{ matrix.cluster.version }}
208-
cluster-name: mlflow-ci-${{ github.run_id }}-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }}
214+
cluster-name: mlflow-ci-${{ github.run_id }}-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }}-${{ matrix.config.name }}
209215
ttl: 1h
210216
export-kubeconfig: true
211217

218+
- name: Expose Application Port
219+
id: expose-port
220+
uses: replicatedhq/replicated-actions/expose-port@main
221+
with:
222+
api-token: ${{ secrets.REPLICATED_PLATFORM_EXAMPLES_TOKEN }}
223+
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
224+
port: '${{ matrix.config.port }}'
225+
protocols: 'http,https'
226+
wildcard: 'false'
227+
timeout-minutes: '5'
228+
212229
- name: Add Helm repositories
213230
run: |
214231
cd applications/mlflow
@@ -231,22 +248,44 @@ jobs:
231248
env:
232249
REPLICATED_LICENSE_ID: ${{ steps.get-license.outputs.license_id }}
233250

234-
- name: Run Helm installation test with charts from Replicated registry
251+
- name: Run Helm installation test with chart-testing
235252
run: |
236253
cd applications/mlflow
237254
# Save kubeconfig to a file
238255
KUBECONFIG_FILE="/tmp/kubeconfig-${{ github.run_id }}"
239256
echo "$KUBECONFIG" > "$KUBECONFIG_FILE"
240257
echo "Saved kubeconfig to $KUBECONFIG_FILE"
241258
242-
# Pass env vars directly to make
243-
KUBECONFIG="$KUBECONFIG_FILE" REPLICATED_APP="$REPLICATED_APP" REPLICATED_CHANNEL="$REPLICATED_CHANNEL" REPLICATED_LICENSE_ID="$REPLICATED_LICENSE_ID" make test-replicated-helm
259+
# Set up environment for the make target
260+
export KUBECONFIG="$KUBECONFIG_FILE"
261+
export REPLICATED_APP="${REPLICATED_APP}"
262+
export REPLICATED_CHANNEL="${REPLICATED_CHANNEL}"
263+
export REPLICATED_LICENSE_ID="${REPLICATED_LICENSE_ID}"
264+
265+
# Use test-specific values file
266+
export MLFLOW_VALUES="${{ matrix.config.values_file }}"
267+
268+
echo "Running test '${{ matrix.config.name }}' with MLflow values file: $MLFLOW_VALUES"
269+
270+
# Run chart testing installation using our updated make target that uses 'ct'
271+
make test-replicated-helm-with-values
244272
env:
245273
KUBECONFIG: ${{ steps.create-cluster.outputs.cluster-kubeconfig }}
246274
REPLICATED_APP: ${{ env.APP_SLUG }}
247275
REPLICATED_CHANNEL: ${{ needs.create-release.outputs.channel-slug }}
248276
REPLICATED_LICENSE_ID: ${{ steps.get-license.outputs.license_id }}
249277

278+
# Application testing with our consolidated test file
279+
- name: Run Application Tests
280+
run: |
281+
cd applications/mlflow
282+
echo "Installing Python dependencies for tests..."
283+
pip install mlflow pandas scikit-learn
284+
285+
echo "Running MLflow application tests against ${{ steps.expose-port.outputs.hostname }}"
286+
python tests/mlflow_test.py 443 --protocol https
287+
if: false # Disabled for now until we're ready to implement application tests
288+
250289
- name: Install troubleshoot
251290
run: curl -L https://github.com/replicatedhq/troubleshoot/releases/latest/download/support-bundle_linux_amd64.tar.gz | tar xzvf -
252291
if: failure()
@@ -258,7 +297,7 @@ jobs:
258297
echo "$KUBECONFIG" > "$KUBECONFIG_FILE"
259298
echo "Saved kubeconfig to $KUBECONFIG_FILE"
260299
261-
./support-bundle --kubeconfig="$KUBECONFIG_FILE" --interactive=false -o ci-bundle-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }} https://raw.githubusercontent.com/replicatedhq/troubleshoot-specs/main/in-cluster/default.yaml
300+
./support-bundle --kubeconfig="$KUBECONFIG_FILE" --interactive=false -o ci-bundle-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }}-${{ matrix.config.name }} https://raw.githubusercontent.com/replicatedhq/troubleshoot-specs/main/in-cluster/default.yaml
262301
if: failure()
263302
env:
264303
KUBECONFIG: ${{ steps.create-cluster.outputs.cluster-kubeconfig }}
@@ -267,8 +306,8 @@ jobs:
267306
uses: actions/upload-artifact@v4
268307
if: failure()
269308
with:
270-
name: mlflow-bundle-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }}
271-
path: 'ci-bundle-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }}.tar.gz'
309+
name: mlflow-bundle-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }}-${{ matrix.config.name }}
310+
path: 'ci-bundle-${{ matrix.cluster.distribution }}-${{ matrix.cluster.version }}-${{ matrix.config.name }}.tar.gz'
272311

273312
- name: Remove Cluster
274313
uses: replicatedhq/replicated-actions/[email protected]

applications/mlflow/Makefile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,44 @@ test-replicated-helm: registry-login
140140
rm -f ct-oci.yaml; \
141141
echo "Replicated Helm installation test completed successfully."
142142

143+
# Target for testing with chart-testing (ct) and custom values
144+
.PHONY: test-replicated-helm-with-values
145+
test-replicated-helm-with-values: registry-login
146+
echo "Running Helm installation test with custom values using chart-testing..."; \
147+
echo "Note: This requires REPLICATED_APP and REPLICATED_CHANNEL env vars."; \
148+
if [ -z "$$REPLICATED_APP" ] || [ -z "$$REPLICATED_CHANNEL" ]; then \
149+
echo "ERROR: REPLICATED_APP and REPLICATED_CHANNEL must be set"; \
150+
exit 1; \
151+
fi; \
152+
OCI_URL="oci://registry.replicated.com/$$REPLICATED_APP/$$REPLICATED_CHANNEL"; \
153+
echo "Creating temporary ct-oci.yaml config file with custom values..."; \
154+
echo "chart-repos:" > ct-oci.yaml; \
155+
echo " - replicated=$$OCI_URL" >> ct-oci.yaml; \
156+
echo "debug: true" >> ct-oci.yaml; \
157+
\
158+
# Prepare values arguments if provided
159+
if [ -n "$$MLFLOW_VALUES" ]; then \
160+
echo "Using MLflow values file: $$MLFLOW_VALUES"; \
161+
echo "helm-extra-args: --values $$MLFLOW_VALUES" >> ct-oci.yaml; \
162+
fi; \
163+
\
164+
cat ct-oci.yaml; \
165+
\
166+
echo "Installing infra chart with chart-testing..."; \
167+
ct install --config ct-oci.yaml \
168+
--charts "infra" \
169+
--namespace default \
170+
--release-label "ci-test"; \
171+
\
172+
echo "Installing mlflow chart with chart-testing..."; \
173+
ct install --config ct-oci.yaml \
174+
--charts "mlflow" \
175+
--namespace default \
176+
--release-label "ci-test"; \
177+
\
178+
rm -f ct-oci.yaml; \
179+
echo "Helm installation with custom values completed successfully."
180+
143181
# Example target to check versions (optional)
144182
.PHONY: check-versions
145183
check-versions:
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Test values for MLflow CI pipeline
2+
# These values specifically configure the service to use NodePort for testing
3+
4+
mlflow:
5+
# Service configuration for MLflow
6+
service:
7+
# Use NodePort to expose the service on a specific port
8+
type: NodePort
9+
# Service port number (internal)
10+
port: 5000
11+
# Hardcoded nodePort for consistent access
12+
# Note: Must be between 30000-32767
13+
nodePort: 30080
14+
# Service port name
15+
name: http
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import os
5+
import argparse
6+
import subprocess
7+
import mlflow
8+
from mlflow.models import infer_signature
9+
10+
import pandas as pd
11+
from sklearn import datasets
12+
from sklearn.model_selection import train_test_split
13+
from sklearn.linear_model import LogisticRegression
14+
from sklearn.metrics import accuracy_score
15+
16+
def run_mlflow_test(tracking_uri):
17+
"""
18+
Run MLflow test with the specified tracking URI
19+
20+
Args:
21+
tracking_uri: The URI to use for the MLflow tracking server
22+
23+
Returns:
24+
True if the test passed, False otherwise
25+
"""
26+
try:
27+
print(f"Setting MLflow tracking URI to: {tracking_uri}")
28+
mlflow.set_tracking_uri(tracking_uri)
29+
30+
# Load the Iris dataset
31+
X, y = datasets.load_iris(return_X_y=True)
32+
33+
# Split the data into training and test sets
34+
X_train, X_test, y_train, y_test = train_test_split(
35+
X, y, test_size=0.2, random_state=42
36+
)
37+
38+
# Define the model hyperparameters
39+
params = {
40+
"solver": "lbfgs",
41+
"max_iter": 1000,
42+
"multi_class": "auto",
43+
"random_state": 8888,
44+
}
45+
46+
# Train the model
47+
lr = LogisticRegression(**params)
48+
lr.fit(X_train, y_train)
49+
50+
# Predict on the test set
51+
y_pred = lr.predict(X_test)
52+
53+
# Calculate metrics
54+
accuracy = accuracy_score(y_test, y_pred)
55+
56+
print("Current tracking URI:", mlflow.get_tracking_uri())
57+
58+
# Create a new MLflow Experiment
59+
mlflow.set_experiment("MLflow CI Test")
60+
61+
# Start an MLflow run
62+
with mlflow.start_run():
63+
# Log the hyperparameters
64+
mlflow.log_params(params)
65+
66+
# Log the loss metric
67+
mlflow.log_metric("accuracy", accuracy)
68+
69+
# Set a tag that we can use to remind ourselves what this run was for
70+
mlflow.set_tag("Training Info", "CI Test for MLflow")
71+
72+
# Infer the model signature
73+
signature = infer_signature(X_train, lr.predict(X_train))
74+
75+
# Log the model
76+
model_info = mlflow.sklearn.log_model(
77+
sk_model=lr,
78+
artifact_path="iris_model",
79+
registered_model_name="ci-test-model",
80+
signature=signature
81+
)
82+
83+
print(f"Model URI: {model_info.model_uri}")
84+
85+
# Load the model back for predictions as a generic Python Function model
86+
try:
87+
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
88+
predictions = loaded_model.predict(X_test[:3])
89+
print(f"Test predictions: {predictions}")
90+
return True
91+
except Exception as e:
92+
print(f"Error loading model: {e}")
93+
return False
94+
95+
except Exception as e:
96+
print(f"Test failed with error: {e}")
97+
return False
98+
99+
def ensure_dependencies():
100+
"""Ensure required packages are installed."""
101+
try:
102+
import mlflow
103+
import pandas
104+
import sklearn
105+
except ImportError:
106+
print("Installing required dependencies...")
107+
subprocess.check_call([
108+
sys.executable, "-m", "pip", "install",
109+
"mlflow", "pandas", "scikit-learn"
110+
])
111+
112+
def main():
113+
parser = argparse.ArgumentParser(description="MLflow CI testing tool")
114+
parser.add_argument("hostname", help="Hostname of the MLflow server")
115+
parser.add_argument("--port", type=int, help="Port number (if not included in hostname)")
116+
parser.add_argument("--protocol", default="https", help="Protocol (http or https, default: https)")
117+
118+
args = parser.parse_args()
119+
120+
# Build the tracking URI
121+
tracking_uri = f"{args.protocol}://{args.hostname}"
122+
if args.port:
123+
tracking_uri += f":{args.port}"
124+
125+
# Ensure dependencies are installed
126+
ensure_dependencies()
127+
128+
# Run the test
129+
success = run_mlflow_test(tracking_uri)
130+
131+
if success:
132+
print("✅ MLflow test completed successfully")
133+
sys.exit(0)
134+
else:
135+
print("❌ MLflow test failed")
136+
sys.exit(1)
137+
138+
if __name__ == "__main__":
139+
main()

0 commit comments

Comments
 (0)