Skip to content

Commit a2f285b

Browse files
committed
Merge branch 'new' into chore/update-kustomize-manifests-structure
# Conflicts: # .github/workflows/test-manifests.yaml
2 parents 9d217ca + e671bbe commit a2f285b

9 files changed

Lines changed: 590 additions & 43 deletions

File tree

.github/workflows/test-manifests.yaml

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
test-models-web-app:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- name: Checkout models-web-app
16+
- name: Checkout Models Web Application
1717
uses: actions/checkout@v4
1818
with:
1919
path: models-web-app
@@ -33,6 +33,23 @@ jobs:
3333
working-directory: manifests
3434
run: kustomize build common/kubeflow-namespace/base | kubectl apply -f -
3535

36+
- name: Create KServe namespace
37+
run: kubectl create namespace kserve --dry-run=client -o yaml | kubectl apply -f -
38+
39+
- name: Build and dry-run apply KServe Models Web Application base manifests
40+
working-directory: models-web-app
41+
run: |
42+
kustomize build manifests/kustomize/base | kubectl apply --dry-run=server -f -
43+
44+
- name: Sync KServe Models Web Application manifests into kubeflow/manifests checkout
45+
run: |
46+
rm -rf manifests/applications/kserve/models-web-app/base
47+
rm -rf manifests/applications/kserve/models-web-app/components
48+
rm -rf manifests/applications/kserve/models-web-app/overlays
49+
cp -R models-web-app/manifests/kustomize/base manifests/applications/kserve/models-web-app/
50+
cp -R models-web-app/manifests/kustomize/components manifests/applications/kserve/models-web-app/
51+
cp -R models-web-app/manifests/kustomize/overlays manifests/applications/kserve/models-web-app/
52+
3653
- name: Install Istio CNI
3754
working-directory: manifests
3855
run: ./tests/istio-cni_install.sh
@@ -70,22 +87,11 @@ jobs:
7087
kubectl wait --for=condition=Available --timeout=120s deployment/cluster-local-gateway -n istio-system
7188
sleep 60
7289
73-
- name: Create kserve namespace
74-
run: kubectl create namespace kserve --dry-run=client -o yaml | kubectl apply -f -
75-
76-
- name: Build and dry-run apply KServe Models Web App base manifests
77-
working-directory: models-web-app
90+
- name: Wait for KServe Models Web Application deployment
7891
run: |
79-
kustomize build manifests/kustomize/base | kubectl apply --dry-run=server -f -
80-
81-
- name: Delete old models-web-app deployment to avoid immutable selector error
82-
run: kubectl delete deployment kserve-models-web-app -n kubeflow --ignore-not-found
83-
84-
- name: Build and apply KServe Models Web App kubeflow overlay manifests
85-
working-directory: models-web-app
86-
run: |
87-
kustomize build manifests/kustomize/overlays/kubeflow | kubectl apply --server-side --force-conflicts -f -
88-
kubectl wait --for=condition=Available deployment/kserve-models-web-app -n kubeflow --timeout=120s
92+
kubectl wait --for=condition=Ready pods --all --all-namespaces --timeout=300s --field-selector=status.phase!=Succeeded
93+
kubectl wait --for=condition=Available deployment/kserve-models-web-app -n kubeflow --timeout=60s
94+
sleep 10
8995
9096
- name: Setup python 3.12
9197
uses: actions/setup-python@v4
@@ -97,8 +103,14 @@ jobs:
97103
run: ./tests/port_forward_gateway.sh
98104

99105
- name: Run KServe tests
100-
working-directory: manifests
101-
run: ./tests/kserve_test.sh kubeflow-user-example-com
106+
run: bash ./models-web-app/manifests/tests/kserve_test.sh kubeflow-user-example-com
107+
108+
- name: Debug KServe Models Web Application connectivity
109+
if: failure()
110+
run: |
111+
kubectl get deployment,service,endpoints,pods -n kubeflow -l app.kubernetes.io/component=kserve-models-web-app || true
112+
kubectl describe deployment kserve-models-web-app -n kubeflow || true
113+
kubectl logs deployment/kserve-models-web-app -n kubeflow --all-containers=true --tail=200 || true
102114
103115
- name: Apply Pod Security Standards restricted levels
104116
working-directory: manifests

manifests/kustomize/base/deployment.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ spec:
77
replicas: 1
88
selector:
99
matchLabels:
10-
app.kubernetes.io/component: models-web-app
10+
app.kubernetes.io/component: kserve-models-web-app
1111
template:
1212
metadata:
1313
labels:
14-
app.kubernetes.io/component: models-web-app
14+
app.kubernetes.io/component: kserve-models-web-app
1515
spec:
1616
securityContext:
1717
runAsNonRoot: true

manifests/kustomize/base/kustomization.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace: kserve
77
images:
88
- name: kserve/models-web-app
99
newName: ghcr.io/kserve/models-web-app
10-
newTag: latest
10+
newTag: 0.15.0
1111
configMapGenerator:
1212
- literals:
1313
- APP_DISABLE_AUTH="True"
@@ -17,9 +17,8 @@ configMapGenerator:
1717
name: kserve-models-web-app-config
1818
apiVersion: kustomize.config.k8s.io/v1beta1
1919
kind: Kustomization
20-
components:
21-
- ../components/common
2220
labels:
2321
- includeSelectors: true
2422
pairs:
25-
app.kubernetes.io/component: models-web-app
23+
app.kubernetes.io/component: kserve-models-web-app
24+
kustomize.component: kserve-models-web-app

manifests/kustomize/base/service.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ metadata:
55
namespace: kserve
66
spec:
77
ports:
8-
- name: http
9-
port: 80
10-
protocol: TCP
11-
targetPort: 5000
8+
- name: http
9+
port: 80
10+
protocol: TCP
11+
targetPort: 5000
1212
selector:
13-
app.kubernetes.io/component: models-web-app
13+
app.kubernetes.io/component: kserve-models-web-app
1414
type: ClusterIP

manifests/kustomize/components/istio/authorization-policy.yaml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ apiVersion: security.istio.io/v1beta1
22
kind: AuthorizationPolicy
33
metadata:
44
labels:
5-
app.kubernetes.io/component: models-web-app
5+
kustomize.component: kserve-models-web-app
6+
app.kubernetes.io/component: kserve-models-web-app
67
name: kserve-models-web-app
78
spec:
89
action: ALLOW
910
selector:
1011
matchLabels:
11-
app.kubernetes.io/component: models-web-app
12+
kustomize.component: kserve-models-web-app
13+
app.kubernetes.io/component: kserve-models-web-app
1214
rules:
13-
- from:
14-
- source:
15-
principals:
16-
- cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account
15+
- from:
16+
- source:
17+
principals:
18+
- cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account

manifests/kustomize/components/istio/kustomization.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,3 @@ resources:
77

88
patches:
99
- path: web-app-sidecar.yaml
10-
11-
labels:
12-
- pairs:
13-
app.kubernetes.io/component: models-web-app

manifests/kustomize/overlays/kubeflow/kustomization.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ resources:
3030

3131
components:
3232
- ../../components/istio
33-
labels:
34-
- includeSelectors: true
35-
pairs:
36-
app: kserve
3733
patches:
3834
- path: patches/web-app-vsvc.yaml
3935
target:
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
"""KServe sklearn prediction test.
15+
16+
Deploys an sklearn InferenceService via the KServe Python SDK,
17+
waits for Ready, runs a prediction via host-based routing,
18+
asserts the output, and cleans up.
19+
20+
This test is fully independent - it does not rely on any
21+
external deployment from kserve_test.sh.
22+
23+
Because of the mesh-wide global-deny-all AuthorizationPolicy
24+
(common/istio/istio-install/base/deny_all_authorizationpolicy.yaml),
25+
the predictor pod's sidecar blocks all traffic by default.
26+
We create a permissive ALLOW AuthorizationPolicy (rules: [{}]) on the
27+
predictor pod. Security is enforced at the ingress gateway, which
28+
validates the JWT via RequestAuthentication before forwarding.
29+
"""
30+
31+
import os
32+
import sys
33+
34+
os.system(
35+
f"{sys.executable} -m pip install -q"
36+
" pytest>=7.0.0 kserve>=0.16.0 kubernetes>=18.20.0 requests>=2.18.4"
37+
)
38+
39+
import json
40+
import logging
41+
import time
42+
43+
import requests
44+
from kubernetes import client
45+
from kubernetes.client import V1ResourceRequirements
46+
47+
from kserve import (
48+
KServeClient,
49+
V1beta1InferenceService,
50+
V1beta1InferenceServiceSpec,
51+
V1beta1PredictorSpec,
52+
V1beta1SKLearnSpec,
53+
constants,
54+
)
55+
56+
logging.basicConfig(level=logging.INFO)
57+
58+
AUTHORIZATION_POLICY_NAME = "allow-isvc-sklearn"
59+
SERVICE_NAME = "isvc-sklearn"
60+
KSERVE_TEST_NAMESPACE = os.environ.get(
61+
"KSERVE_TEST_NAMESPACE", "kubeflow-user-example-com"
62+
)
63+
64+
IRIS_INPUT = {"instances": [[6.8, 2.8, 4.8, 1.4], [6.0, 3.4, 4.5, 1.6]]}
65+
66+
67+
class M2mTokenNotAvailable(Exception):
68+
pass
69+
70+
71+
def get_cluster_ip(name="istio-ingressgateway", namespace="istio-system"):
72+
api_instance = client.CoreV1Api(client.ApiClient())
73+
service = api_instance.read_namespaced_service(name, namespace)
74+
if service.status.load_balancer.ingress is None:
75+
cluster_ip = service.spec.cluster_ip
76+
else:
77+
if service.status.load_balancer.ingress[0].hostname:
78+
cluster_ip = service.status.load_balancer.ingress[0].hostname
79+
else:
80+
cluster_ip = service.status.load_balancer.ingress[0].ip
81+
return os.environ.get("KSERVE_INGRESS_HOST_PORT", cluster_ip)
82+
83+
84+
def get_m2m_auth_token(env_name="KSERVE_M2M_TOKEN"):
85+
try:
86+
return os.environ[env_name]
87+
except KeyError:
88+
raise M2mTokenNotAvailable(env_name)
89+
90+
91+
def predict(service_name, input_data):
92+
"""Send a prediction request using host-based routing."""
93+
kfs_client = KServeClient(
94+
config_file=os.environ.get("KUBECONFIG", "~/.kube/config")
95+
)
96+
kfs_client.get(
97+
service_name,
98+
namespace=KSERVE_TEST_NAMESPACE,
99+
version=constants.KSERVE_V1BETA1_VERSION,
100+
)
101+
time.sleep(10)
102+
cluster_ip = get_cluster_ip()
103+
104+
host = f"{service_name}.{KSERVE_TEST_NAMESPACE}.example.com"
105+
headers = {
106+
"Host": host,
107+
"Content-Type": "application/json",
108+
}
109+
110+
try:
111+
token = get_m2m_auth_token()
112+
headers["Authorization"] = f"Bearer {token}"
113+
logging.info("M2M Token Found.")
114+
except M2mTokenNotAvailable:
115+
logging.warning("M2M Token Not found, client authentication disabled.")
116+
117+
url = f"http://{cluster_ip}/v1/models/{service_name}:predict"
118+
119+
logging.info("Sending Header = %s", headers)
120+
logging.info("Sending url = %s", url)
121+
logging.info("Sending request data: %s", input_data)
122+
response = requests.post(url, json.dumps(input_data), headers=headers)
123+
logging.info(
124+
"Got response code %s, content %s", response.status_code, response.content
125+
)
126+
if response.status_code == 200:
127+
return json.loads(response.content.decode("utf-8"))
128+
response.raise_for_status()
129+
130+
131+
def create_predictor_authorization_policy(namespace):
132+
"""Create an AuthorizationPolicy allowing traffic to the predictor pod."""
133+
api = client.CustomObjectsApi()
134+
ap_body = {
135+
"apiVersion": "security.istio.io/v1beta1",
136+
"kind": "AuthorizationPolicy",
137+
"metadata": {
138+
"name": AUTHORIZATION_POLICY_NAME,
139+
"namespace": namespace,
140+
},
141+
"spec": {
142+
"action": "ALLOW",
143+
"rules": [{}],
144+
"selector": {
145+
"matchLabels": {
146+
"serving.knative.dev/service": f"{SERVICE_NAME}-predictor",
147+
}
148+
},
149+
},
150+
}
151+
api.create_namespaced_custom_object(
152+
group="security.istio.io",
153+
version="v1beta1",
154+
namespace=namespace,
155+
plural="authorizationpolicies",
156+
body=ap_body,
157+
)
158+
logging.info(
159+
"Created AuthorizationPolicy %s in %s",
160+
AUTHORIZATION_POLICY_NAME,
161+
namespace,
162+
)
163+
164+
165+
def delete_predictor_authorization_policy(namespace):
166+
"""Delete the predictor AuthorizationPolicy."""
167+
api = client.CustomObjectsApi()
168+
try:
169+
api.delete_namespaced_custom_object(
170+
group="security.istio.io",
171+
version="v1beta1",
172+
namespace=namespace,
173+
plural="authorizationpolicies",
174+
name=AUTHORIZATION_POLICY_NAME,
175+
)
176+
logging.info(
177+
"Deleted AuthorizationPolicy %s in %s",
178+
AUTHORIZATION_POLICY_NAME,
179+
namespace,
180+
)
181+
except client.exceptions.ApiException as exc:
182+
if exc.status != 404:
183+
raise
184+
185+
186+
def test_sklearn_kserve():
187+
predictor = V1beta1PredictorSpec(
188+
min_replicas=1,
189+
sklearn=V1beta1SKLearnSpec(
190+
storage_uri="gs://kfserving-examples/models/sklearn/1.0/model",
191+
resources=V1ResourceRequirements(
192+
requests={"cpu": "50m", "memory": "128Mi"},
193+
limits={"cpu": "100m", "memory": "256Mi"},
194+
),
195+
),
196+
)
197+
198+
isvc = V1beta1InferenceService(
199+
api_version=constants.KSERVE_V1BETA1,
200+
kind="InferenceService",
201+
metadata=client.V1ObjectMeta(
202+
name=SERVICE_NAME, namespace=KSERVE_TEST_NAMESPACE
203+
),
204+
spec=V1beta1InferenceServiceSpec(predictor=predictor),
205+
)
206+
207+
kserve_client = KServeClient(
208+
config_file=os.environ.get("KUBECONFIG", "~/.kube/config")
209+
)
210+
211+
try:
212+
create_predictor_authorization_policy(KSERVE_TEST_NAMESPACE)
213+
214+
kserve_client.create(isvc)
215+
kserve_client.wait_isvc_ready(
216+
SERVICE_NAME, namespace=KSERVE_TEST_NAMESPACE
217+
)
218+
219+
response = predict(SERVICE_NAME, IRIS_INPUT)
220+
assert response["predictions"] == [1, 1]
221+
logging.info(
222+
"Python SDK prediction passed for %s in %s",
223+
SERVICE_NAME,
224+
KSERVE_TEST_NAMESPACE,
225+
)
226+
finally:
227+
kserve_client.delete(SERVICE_NAME, KSERVE_TEST_NAMESPACE)
228+
delete_predictor_authorization_policy(KSERVE_TEST_NAMESPACE)

0 commit comments

Comments
 (0)