From 1eb093b01fffb944ce0de4262701b3ce8f789707 Mon Sep 17 00:00:00 2001 From: Jin Chi He Date: Thu, 26 Sep 2019 23:05:07 +0800 Subject: [PATCH] adding repalce and promote APIs for kfserving sdk (#357) * adding replace api for kfserving sdk * rollback replace api --- .../samples/client/kfserving_sdk_sample.ipynb | 10 +-- python/kfserving/docs/KFServingClient.md | 86 ++++++++++++++++++- .../kfserving/api/kf_serving_client.py | 64 ++++++++++++++ .../kfserving/test/test_kfservice_client.py | 12 +++ 4 files changed, 162 insertions(+), 10 deletions(-) diff --git a/docs/samples/client/kfserving_sdk_sample.ipynb b/docs/samples/client/kfserving_sdk_sample.ipynb index 4f476a13730..4f777654819 100644 --- a/docs/samples/client/kfserving_sdk_sample.ipynb +++ b/docs/samples/client/kfserving_sdk_sample.ipynb @@ -171,15 +171,7 @@ "metadata": {}, "outputs": [], "source": [ - "kfsvc = V1alpha2KFService(api_version=api_version,\n", - " kind=constants.KFSERVING_KIND,\n", - " metadata=client.V1ObjectMeta(\n", - " name='flower-sample', namespace='kubeflow'),\n", - " spec=V1alpha2KFServiceSpec(default=canary_endpoint_spec,\n", - " canary=None,\n", - " canary_traffic_percent=0))\n", - "\n", - "KFServing.patch('flower-sample', kfsvc)" + "KFServing.promote('flower-sample', namespace='kubeflow')" ] }, { diff --git a/python/kfserving/docs/KFServingClient.md b/python/kfserving/docs/KFServingClient.md index 33c4f1c03cd..014338737bc 100644 --- a/python/kfserving/docs/KFServingClient.md +++ b/python/kfserving/docs/KFServingClient.md @@ -20,6 +20,8 @@ KFServingClient | [set_credentials](#set_credentials) | Set Credentials| KFServingClient | [create](#create) | Create KFService| KFServingClient | [get](#get) | Get or watch the specified KFService or all KFServices in the namespace | KFServingClient | [patch](#patch) | Patch the specified KFService| +KFServingClient | [replace](#replace) | Replace the specified KFService| +KFServingClient | [promote](#promote) | Promote the `canary` version of the KFService to `default`| KFServingClient | [delete](#delete) | Delete the specified KFService | ## set_credentials @@ -177,7 +179,9 @@ object ## patch > patch(name, kfservice, namespace=None, watch=False, timeout_seconds=600) -Patch the created KFService in the specified namespace +Patch the created KFService in the specified namespace. + +Note that if you want to set the field from existing value to `None`, `patch` API may not work, you need to use [replace](#replace) API to remove the field value. ### Example @@ -221,6 +225,86 @@ timeout_seconds | int | Timeout seconds for watching. Defaults to 600. | Optiona ### Return type object +## replace +> replace(name, kfservice, namespace=None, watch=False, timeout_seconds=600) + +Replace the created KFService in the specified namespace. Generally use the `replace` API to update whole KFService or remove a field such as canary or other components of the KFService. + +### Example + +```python +from kubernetes import client +from kfserving import constants +from kfserving import V1alpha2EndpointSpec +from kfserving import V1alpha2PredictorSpec +from kfserving import V1alpha2TensorflowSpec +from kfserving import V1alpha2KFServiceSpec +from kfserving import V1alpha2KFService +from kfserving import KFServingClient + +default_endpoint_spec = V1alpha2EndpointSpec( + predictor=V1alpha2PredictorSpec( + tensorflow=V1alpha2TensorflowSpec( + storage_uri='gs://kfserving-samples/models/tensorflow/flowers', + resources=None))) + +kfsvc = V1alpha2KFService(api_version=api_version, + kind=constants.KFSERVING_KIND, + metadata=client.V1ObjectMeta( + name='flower-sample', + namespace='kubeflow', + resource_version=resource_version), + spec=V1alpha2KFServiceSpec(default=default_endpoint_spec, + canary=None, + canary_traffic_percent=0)) + + +KFServing = KFServingClient() +KFServing.replace('flower-sample', kfsvc) + +# The API also supports watching the replaced KFService status till it's READY. +# KFServing.replace('flower-sample', kfsvc, watch=True) +``` + +### Parameters +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +kfservice | [V1alpha2KFService](V1alpha2KFService.md) | KFService defination| Required | +namespace | str | The KFService's namespace. If the `namespace` is not defined, will align with KFService definition, or use current or default namespace if namespace is not specified in KFService definition. | Optional| +watch | bool | Watch the patched KFService if `True`, otherwise will return the replaced KFService object. Stop watching if KFService reaches the optional specified `timeout_seconds` or once the KFService overall status `READY` is `True`. | Optional | +timeout_seconds | int | Timeout seconds for watching. Defaults to 600. | Optional | + +### Return type +object + + +## promote +> promote(name, namespace=None, watch=False, timeout_seconds=600) + +Promote the `Canary KFServiceSpec` to `Default KFServiceSpec` for the created KFService in the specified namespace. + +### Example + +```python + +KFServing = KFServingClient() +KFServing.promote('flower-sample', namespace='kubeflow') + +# The API also supports watching the promoted KFService status till it's READY. +# KFServing.promote('flower-sample', namespace='kubeflow', watch=True) +``` + +### Parameters +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +Name | str | The KFService name for promoting.| | +namespace | str | The KFService's namespace. If the `namespace` is not defined, will align with KFService definition, or use current or default namespace if namespace is not specified in KFService definition. | Optional| +watch | bool | Watch the promoted KFService if `True`, otherwise will return the promoted KFService object. Stop watching if KFService reaches the optional specified `timeout_seconds` or once the KFService overall status `READY` is `True`. | Optional | +timeout_seconds | int | Timeout seconds for watching. Defaults to 600. | Optional | + +### Return type +object + ## delete > delete(name, namespace=None) diff --git a/python/kfserving/kfserving/api/kf_serving_client.py b/python/kfserving/kfserving/api/kf_serving_client.py index 3d8199de7d9..8c1991f49ec 100644 --- a/python/kfserving/kfserving/api/kf_serving_client.py +++ b/python/kfserving/kfserving/api/kf_serving_client.py @@ -18,6 +18,8 @@ from ..utils import utils from .creds_utils import set_gcs_credentials, set_s3_credentials, set_azure_credentials from .kf_serving_watch import watch as kfsvc_watch +from ..models.v1alpha2_kf_service import V1alpha2KFService +from ..models.v1alpha2_kf_service_spec import V1alpha2KFServiceSpec class KFServingClient(object): @@ -167,6 +169,68 @@ def patch(self, name, kfservice, namespace=None, watch=False, timeout_seconds=60 else: return outputs + def replace(self, name, kfservice, namespace=None, watch=False, timeout_seconds=600): # pylint:disable=too-many-arguments,inconsistent-return-statements + """Replace the created KFService in the specified namespace""" + + if namespace is None: + namespace = utils.set_kfsvc_namespace(kfservice) + + if kfservice.metadata.resource_version is None: + current_kfsvc = self.get(name, namespace=namespace) + current_resource_version = current_kfsvc['metadata']['resourceVersion'] + kfservice.metadata.resource_version = current_resource_version + + try: + outputs = self.api_instance.replace_namespaced_custom_object( + constants.KFSERVING_GROUP, + constants.KFSERVING_VERSION, + namespace, + constants.KFSERVING_PLURAL, + name, + kfservice) + except client.rest.ApiException as e: + raise RuntimeError( + "Exception when calling CustomObjectsApi->replace_namespaced_custom_object:\ + %s\n" % e) + + if watch: + kfsvc_watch( + name=outputs['metadata']['name'], + namespace=namespace, + timeout_seconds=timeout_seconds) + else: + return outputs + + + def promote(self, name, namespace=None, watch=False, timeout_seconds=600): # pylint:disable=too-many-arguments,inconsistent-return-statements + """Promote the created KFService in the specified namespace""" + + if namespace is None: + namespace = utils.get_default_target_namespace() + + current_kfsvc = self.get(name, namespace=namespace) + api_version = current_kfsvc['apiVersion'] + + try: + current_canary_spec = current_kfsvc['spec']['canary'] + except KeyError: + raise RuntimeError("Cannot promote a KFService that has no Canary Spec.") + + kfservice = V1alpha2KFService( + api_version=api_version, + kind=constants.KFSERVING_KIND, + metadata=client.V1ObjectMeta( + name=name, + namespace=namespace), + spec=V1alpha2KFServiceSpec( + default=current_canary_spec, + canary=None, + canary_traffic_percent=0)) + + return self.replace(name=name, kfservice=kfservice, namespace=namespace, + watch=watch, timeout_seconds=timeout_seconds) + + def delete(self, name, namespace=None): """Delete the provided KFService in the specified namespace""" diff --git a/python/kfserving/test/test_kfservice_client.py b/python/kfserving/test/test_kfservice_client.py index d418452d1cb..8560669c063 100644 --- a/python/kfserving/test/test_kfservice_client.py +++ b/python/kfserving/test/test_kfservice_client.py @@ -90,6 +90,18 @@ def test_kfservice_client_patch(): kfsvc = generate_kfservice() assert mocked_unit_result == KFServing.patch('flower-sample', kfsvc, namespace='kubeflow') +def test_kfservice_client_promote(): + '''Unit test for kfserving promote api''' + with patch('kfserving.api.kf_serving_client.KFServingClient.promote', + return_value=mocked_unit_result): + assert mocked_unit_result == KFServing.promote('flower-sample', namespace='kubeflow') + +def test_kfservice_client_replace(): + '''Unit test for kfserving replace api''' + with patch('kfserving.api.kf_serving_client.KFServingClient.replace', + return_value=mocked_unit_result): + kfsvc = generate_kfservice() + assert mocked_unit_result == KFServing.replace('flower-sample', kfsvc, namespace='kubeflow') def test_kfservice_client_delete(): '''Unit test for kfserving delete api'''