This is an automated email from the ASF dual-hosted git repository. liuxun pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/submarine.git
The following commit(s) were added to refs/heads/master by this push: new b2c7e81f SUBMARINE-1315. Fix model can not serve error (#992) b2c7e81f is described below commit b2c7e81f0123b736fc16cc498886c6062e68a4fb Author: Thinking Chen <cdmikec...@hotmail.com> AuthorDate: Tue Sep 13 18:51:22 2022 +0800 SUBMARINE-1315. Fix model can not serve error (#992) ### What is this PR for? The model serve now registers the wrong namespace for resources, and taking the model name directly as a resource name can lead to problems with illegal characters.  ### What type of PR is it? Bug Fix ### Todos * [x] - Model serve codes refactoring. * [x] - Replace default to submarine-server namespace. * [x] - Add a primary key id for `registered_model`(model_id) and `model_version` (model_version_id) * [x] - Replace model service name to `submarine-model-${id}-@{modelId}` in k8s resource metadata name. * [x] - Replace SeldonGraph container name to `version-${modelVersion}`. * [x] - Replace ingress routing to `/seldon/${namespace}/${modelVersionId}/${modelVersion}/` and add owner reference for cascaded deletion of operator. * [x] - Add save_model method and serve predictions example in example quick-start . ### What is the Jira issue? https://issues.apache.org/jira/projects/SUBMARINE/issues/SUBMARINE-1315 ### How should this be tested? Add a simple test case to test k8s resource. ### Screenshots (if appropriate) There is still a legacy of problems. Now that seldon automatically creates the serve services (http and rpc) using istio, the `VirtualService` we create for our own serve will be duplicated, and the url provided by the seldon returned by calling swagger with our own `VirtualService` (like http://localhost:32080/seldon/submarine-user-test/1/1/api/v1.0/doc/) is still seldon http `VirtualService` url. The first was created by us and the following two by seldon. Both are currently accessible.  Swagger UI  ### Questions: * Do the license files need updating? No * Are there breaking changes for older versions? Yes * Does this need new documentation? No --- dev-support/database/submarine-model.sql | 26 +++-- .../examples/quickstart/serve_predictions.py | 112 ++++++++++++++++++ dev-support/examples/quickstart/train.py | 2 + submarine-serve/pom.xml | 12 ++ .../serve/istio/IstioHTTPDestination.java | 45 ++++++++ .../serve/istio/IstioHTTPMatchRequest.java | 27 +++++ .../submarine/serve/istio/IstioHTTPRoute.java | 39 ++++++- .../submarine/serve/istio/IstioVirtualService.java | 9 +- .../serve/istio/IstioVirtualServiceList.java | 21 +++- .../serve/istio/IstioVirtualServiceSpec.java | 34 +++++- ...donPredictor.java => PredictorAnnotations.java} | 63 +++++------ .../submarine/serve/seldon/SeldonDeployment.java | 125 ++++++++++++++++----- ...donPredictor.java => SeldonDeploymentSpec.java} | 47 ++++---- .../apache/submarine/serve/seldon/SeldonGraph.java | 19 ++++ .../submarine/serve/seldon/SeldonPredictor.java | 15 +++ .../{ => seldon}/pytorch/SeldonPytorchServing.java | 20 ++-- .../{ => seldon}/tensorflow/SeldonTFServing.java | 20 ++-- .../submarine/serve/utils/SeldonConstants.java | 2 - .../submarine/server/api/model/ServeSpec.java | 10 ++ submarine-server/server-core/pom.xml | 10 ++ .../model/entities/ModelVersionEntity.java | 14 ++- .../model/entities/RegisteredModelEntity.java | 13 ++- .../database/mappers/ModelVersionMapper.xml | 6 +- .../database/mappers/RegisteredModelMapper.xml | 6 +- .../server-submitter/submitter-k8s/pom.xml | 2 +- .../server/submitter/k8s/K8sSubmitter.java | 75 +++---------- .../k8s/model/istio/IstioVirtualService.java | 10 +- .../k8s/model/seldon/SeldonDeploymentFactory.java | 55 +++++++++ .../seldon/SeldonDeploymentPytorchServing.java | 107 ++++++++++++++++++ .../model/seldon/SeldonDeploymentTFServing.java | 106 +++++++++++++++++ .../submitter/k8s/model/seldon/SeldonResource.java | 27 ++--- .../server/submitter/k8s/util/YamlUtils.java | 14 +++ .../k8s/seldon/SeldonDeploymentResourceTest.java | 86 ++++++++++++++ .../workbench-web/src/app/interfaces/model-info.ts | 4 +- .../src/app/interfaces/model-serve.ts | 4 +- .../model/model-info/model-info.component.html | 8 +- .../model/model-info/model-info.component.ts | 20 ++-- .../src/app/services/model-serve.service.ts | 8 +- 38 files changed, 994 insertions(+), 229 deletions(-) diff --git a/dev-support/database/submarine-model.sql b/dev-support/database/submarine-model.sql index 0726323a..b3b718c0 100644 --- a/dev-support/database/submarine-model.sql +++ b/dev-support/database/submarine-model.sql @@ -11,17 +11,28 @@ -- See the License for the specific language governing permissions and -- limitations under the License. +-- Since the associated tables have primary and foreign key constraints, +-- we need to delete them all before creating them +DROP TABLE IF EXISTS `param`; +DROP TABLE IF EXISTS `metric`; +DROP TABLE IF EXISTS `model_version_tag`; +DROP TABLE IF EXISTS `model_version`; +DROP TABLE IF EXISTS `registered_model_tag`; DROP TABLE IF EXISTS `registered_model`; + +-- Add model_id primary key +-- It is currently designed to solve some of the problems with the model serve, +-- and does not solve the same name problem for multiple users at the moment CREATE TABLE `registered_model` ( + `model_id` BIGINT AUTO_INCREMENT, `name` VARCHAR(256) NOT NULL, `creation_time` DATETIME(3) COMMENT 'Millisecond precision', `last_updated_time` DATETIME(3) COMMENT 'Millisecond precision', `description` VARCHAR(5000), - CONSTRAINT `registered_model_pk` PRIMARY KEY (`name`), + CONSTRAINT `registered_model_pk` PRIMARY KEY (`model_id`), UNIQUE (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `registered_model_tag`; CREATE TABLE `registered_model_tag` ( `name` VARCHAR(256) NOT NULL, `tag` VARCHAR(256) NOT NULL, @@ -29,8 +40,11 @@ CREATE TABLE `registered_model_tag` ( FOREIGN KEY(`name`) REFERENCES `registered_model` (`name`) ON UPDATE CASCADE ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `model_version`; +-- Add model_version_id primary key +-- It is currently designed to solve some of the problems with the model serve, +-- and does not solve the same name problem for multiple users at the moment CREATE TABLE `model_version` ( + `model_version_id` BIGINT AUTO_INCREMENT, `name` VARCHAR(256) NOT NULL COMMENT 'Name of model', `version` INTEGER NOT NULL, `id` VARCHAR(64) NOT NULL COMMENT 'Id of the model', @@ -42,12 +56,12 @@ CREATE TABLE `model_version` ( `last_updated_time` DATETIME(3) COMMENT 'Millisecond precision', `dataset` VARCHAR(256) COMMENT 'Which dataset is used', `description` VARCHAR(5000), - CONSTRAINT `model_version_pk` PRIMARY KEY (`name`, `version`), + CONSTRAINT `model_version_pk` PRIMARY KEY (`model_version_id`), + UNIQUE (`name`, `version`), UNIQUE (`name`, `id`), FOREIGN KEY(`name`) REFERENCES `registered_model` (`name`) ON UPDATE CASCADE ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `model_version_tag`; CREATE TABLE `model_version_tag` ( `name` VARCHAR(256) NOT NULL COMMENT 'Name of model', `version` INTEGER NOT NULL, @@ -56,7 +70,6 @@ CREATE TABLE `model_version_tag` ( FOREIGN KEY(`name`, `version`) REFERENCES `model_version` (`name`, `version`) ON UPDATE CASCADE ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `metric`; CREATE TABLE `metric` ( `id` VARCHAR(64) NOT NULL COMMENT 'Id of the Experiment', `key` VARCHAR(190) NOT NULL COMMENT 'Metric key: `String` (limit 190 characters). Part of *Primary Key* for ``metric`` table.', @@ -70,7 +83,6 @@ CREATE TABLE `metric` ( FOREIGN KEY(`id`) REFERENCES `experiment` (`id`) ON UPDATE CASCADE ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `param`; CREATE TABLE `param` ( `id` VARCHAR(64) NOT NULL COMMENT 'Id of the Experiment', `key` VARCHAR(190) NOT NULL COMMENT '`String` (limit 190 characters). Part of *Primary Key* for ``param`` table.', diff --git a/dev-support/examples/quickstart/serve_predictions.py b/dev-support/examples/quickstart/serve_predictions.py new file mode 100644 index 00000000..e2e90400 --- /dev/null +++ b/dev-support/examples/quickstart/serve_predictions.py @@ -0,0 +1,112 @@ +# Copyright 2020 The Kubeflow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +""" +The code is mainly referenced from: +https://docs.seldon.io/projects/seldon-core/en/latest/examples/tfserving_mnist.html +https://www.tensorflow.org/tfx/tutorials/serving/rest_simple +And the parameters of the predictions call have been modified. +""" + +import numpy as np +import requests +import tensorflow as tf +import tensorflow_datasets as tfds +from matplotlib import pyplot as plt +from packaging.version import Version + + +def show_image(image): + """ + show the image + """ + two_d = (np.reshape(image, (28, 28)) * 255).astype(np.uint8) + plt.imshow(two_d, cmap=plt.cm.gray_r, interpolation="nearest") + plt.show() + + +def rest_request_ambassador(endpoint="localhost:32080", prefix="/", arr=None): + """ + request ambassador with rest + """ + from tensorflow.python.ops.numpy_ops import np_config + + np_config.enable_numpy_behavior() + """ + you can use `saved_model_cli` command to show model inputs and outputs: + saved_model_cli show --dir ${model_path} --all + + MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs: + + signature_def['__saved_model_init_op']: + The given SavedModel SignatureDef contains the following input(s): + The given SavedModel SignatureDef contains the following output(s): + outputs['__saved_model_init_op'] tensor_info: + dtype: DT_INVALID + shape: unknown_rank + name: NoOp + Method name is: + + signature_def['serving_default']: + The given SavedModel SignatureDef contains the following input(s): + inputs['conv2d_input'] tensor_info: + dtype: DT_FLOAT + shape: (-1, 28, 28, 1) + name: serving_default_conv2d_input:0 + The given SavedModel SignatureDef contains the following output(s): + outputs['dense_1'] tensor_info: + dtype: DT_FLOAT + shape: (-1, 10) + name: StatefulPartitionedCall:0 + Method name is: tensorflow/serving/predict + """ + payload = { + "data": { + "names": ["image_predictions"], + "tensor": {"shape": [-1, 28, 28, 1], "values": arr.tolist()}, + } + } + response = requests.post( + # you can also find a swagger ui in http://endpoint/prefix/api/v0.1/doc/ + "http://" + endpoint + prefix + "api/v0.1/predictions", + json=payload, + ) + print(response.text) + # get the prediction + print(f'TThe prediction is {np.argmax(response.json()["data"]["tensor"]["values"])}.') + + +# download datasets +if Version(tfds.__version__) > Version("3.1.0"): + tfds.core.utils.gcs_utils._is_gcs_disabled = True +datasets, _ = tfds.load(name="mnist", with_info=True, as_supervised=True) + + +def scale(image, label): + image = tf.cast(image, tf.float32) + image /= 255 + return image, label + + +# get first dataset +first_dataset = datasets["train"].map(scale).take(1) +for image, label in first_dataset: + show_image(image) + rest_request_ambassador( + endpoint="localhost:32080", + # This prefix you can find in VirtualService in istio, command like: + # kubectl describe VirtualService -n submarine-user-test -l model-name=${model_name} + prefix="/seldon/submarine-user-test/1/1/", + arr=image, + ) diff --git a/dev-support/examples/quickstart/train.py b/dev-support/examples/quickstart/train.py index d0d1b041..51d92cf0 100644 --- a/dev-support/examples/quickstart/train.py +++ b/dev-support/examples/quickstart/train.py @@ -86,6 +86,8 @@ def main(): submarine.log_metric("accuracy", logs["accuracy"], epoch) multi_worker_model.fit(ds_train, epochs=10, steps_per_epoch=70, callbacks=[MyCallback()]) + # save model + submarine.save_model(multi_worker_model, "tensorflow") if __name__ == "__main__": diff --git a/submarine-serve/pom.xml b/submarine-serve/pom.xml index ef4ef9be..f29d3b7c 100644 --- a/submarine-serve/pom.xml +++ b/submarine-serve/pom.xml @@ -46,12 +46,24 @@ <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> + <dependency> <groupId>org.apache.submarine</groupId> <artifactId>submarine-commons-utils</artifactId> <version>0.8.0-SNAPSHOT</version> <scope>test</scope> </dependency> + + <dependency> + <groupId>org.apache.submarine</groupId> + <artifactId>submarine-k8s-utils</artifactId> + <version>0.8.0-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + </dependency> </dependencies> <build> diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPDestination.java b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPDestination.java index 93b888bf..ba416b25 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPDestination.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPDestination.java @@ -22,23 +22,32 @@ import com.google.gson.annotations.SerializedName; import org.apache.submarine.serve.utils.IstioConstants; public class IstioHTTPDestination { + @SerializedName("destination") private IstioDestination destination; + public IstioHTTPDestination() { + } + public IstioHTTPDestination(String host) { this.destination = new IstioDestination(host); } + public IstioHTTPDestination(String host, Integer port) { this.destination = new IstioDestination(host, port); } public static class IstioDestination { + @SerializedName("host") private String host; @SerializedName("port") private IstioPort port; + public IstioDestination() { + } + public IstioDestination(String host) { this(host, IstioConstants.DEFAULT_SERVE_POD_PORT); } @@ -47,14 +56,50 @@ public class IstioHTTPDestination { this.host = host; this.port = new IstioPort(port); } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public IstioPort getPort() { + return port; + } + + public void setPort(IstioPort port) { + this.port = port; + } } public static class IstioPort { + @SerializedName("number") private Integer number; + public IstioPort() { + } + public IstioPort(Integer port) { this.number = port; } + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + } + + public IstioDestination getDestination() { + return destination; + } + + public void setDestination(IstioDestination destination) { + this.destination = destination; } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPMatchRequest.java b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPMatchRequest.java index 18e0b6c5..f587064a 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPMatchRequest.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPMatchRequest.java @@ -18,22 +18,49 @@ */ package org.apache.submarine.serve.istio; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; public class IstioHTTPMatchRequest { + @SerializedName("uri") + @JsonProperty("uri") private IstioPrefix prefix; + public IstioHTTPMatchRequest() { + } + public IstioHTTPMatchRequest(String prefix) { this.prefix = new IstioPrefix(prefix); } public static class IstioPrefix { + @SerializedName("prefix") + @JsonProperty("prefix") private String path; + public IstioPrefix() { + } + public IstioPrefix(String path){ this.path = path; } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + } + + public IstioPrefix getPrefix() { + return prefix; + } + + public void setPrefix(IstioPrefix prefix) { + this.prefix = prefix; } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPRoute.java b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPRoute.java index f370d213..b5d0ded9 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPRoute.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPRoute.java @@ -18,6 +18,7 @@ */ package org.apache.submarine.serve.istio; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; import org.apache.submarine.serve.utils.IstioConstants; @@ -25,6 +26,7 @@ import java.util.ArrayList; import java.util.List; public class IstioHTTPRoute { + @SerializedName("match") private List<IstioHTTPMatchRequest> match = new ArrayList<>(); @@ -46,7 +48,6 @@ public class IstioHTTPRoute { @Override public String toString() { return "'rewrite': {'uri': " + rewrite.getRewrite() + "}"; - } public void addHTTPMatchRequest(IstioHTTPMatchRequest match){ @@ -56,8 +57,11 @@ public class IstioHTTPRoute { public void addHTTPDestination(IstioHTTPDestination destination){ this.route.add(destination); } - public static class IstioRewrite{ + + public static class IstioRewrite { + @SerializedName("uri") + @JsonProperty("uri") private String uri; public IstioRewrite(String rewrite){ @@ -67,6 +71,37 @@ public class IstioHTTPRoute { public String getRewrite() { return uri; } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + } + + public List<IstioHTTPMatchRequest> getMatch() { + return match; + } + + public void setMatch(List<IstioHTTPMatchRequest> match) { + this.match = match; + } + + public List<IstioHTTPDestination> getRoute() { + return route; } + public void setRoute(List<IstioHTTPDestination> route) { + this.route = route; + } + + public IstioRewrite getRewrite() { + return rewrite; + } + + public void setRewrite(IstioRewrite rewrite) { + this.rewrite = rewrite; + } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualService.java b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualService.java index 75b0b1d9..608d95a4 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualService.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualService.java @@ -22,6 +22,7 @@ import com.google.gson.annotations.SerializedName; import io.kubernetes.client.common.KubernetesObject; import io.kubernetes.client.openapi.models.V1ObjectMeta; import org.apache.submarine.serve.utils.IstioConstants; +import org.apache.submarine.server.k8s.utils.K8sUtils; public class IstioVirtualService implements KubernetesObject { @SerializedName("apiVersion") @@ -48,12 +49,12 @@ public class IstioVirtualService implements KubernetesObject { this.spec = spec; } - public IstioVirtualService(String modelName, Integer modelVersion) { + public IstioVirtualService(Long id, String modelResourceName, Integer modelVersion) { V1ObjectMeta metadata = new V1ObjectMeta(); - metadata.setName(modelName); - metadata.setNamespace(IstioConstants.DEFAULT_NAMESPACE); + metadata.setName(modelResourceName); + metadata.setNamespace(K8sUtils.getNamespace()); setMetadata(metadata); - setSpec(new IstioVirtualServiceSpec(modelName, modelVersion)); + setSpec(new IstioVirtualServiceSpec(id, modelResourceName, modelVersion)); } public IstioVirtualService(V1ObjectMeta metadata) { diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceList.java b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceList.java index ea33a2b0..22eb36a5 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceList.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceList.java @@ -27,7 +27,10 @@ import io.kubernetes.client.common.KubernetesObject; import io.kubernetes.client.openapi.models.V1ListMeta; import org.apache.submarine.serve.utils.IstioConstants; -public class IstioVirtualServiceList implements KubernetesListObject{ +public class IstioVirtualServiceList implements KubernetesListObject { + + public IstioVirtualServiceList() { + } @SerializedName("apiVersion") private String apiVersion; @@ -60,4 +63,20 @@ public class IstioVirtualServiceList implements KubernetesListObject{ public String getKind() { return IstioConstants.KIND + "List"; } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public void setMetadata(V1ListMeta metadata) { + this.metadata = metadata; + } + + public void setItems(List<IstioVirtualService> items) { + this.items = items; + } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceSpec.java b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceSpec.java index e8182cbb..bf63bc42 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceSpec.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioVirtualServiceSpec.java @@ -19,30 +19,38 @@ package org.apache.submarine.serve.istio; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; import org.apache.submarine.serve.utils.IstioConstants; +import org.apache.submarine.server.k8s.utils.K8sUtils; import java.util.ArrayList; import java.util.List; public class IstioVirtualServiceSpec { + @SerializedName("hosts") private List<String> hosts = new ArrayList<>(); @SerializedName("gateways") private List<String> gateways = new ArrayList<>(); @SerializedName("http") + @JsonProperty("http") private List<IstioHTTPRoute> httpRoute = new ArrayList<>(); public IstioVirtualServiceSpec() { } - public IstioVirtualServiceSpec(String modelName, Integer modelVersion) { + public IstioVirtualServiceSpec(Long id, String modelResourceName, Integer modelVersion) { hosts.add(IstioConstants.DEFAULT_INGRESS_HOST); gateways.add(IstioConstants.DEFAULT_GATEWAY); - IstioHTTPDestination destination = new IstioHTTPDestination( - modelName + "-" + IstioConstants.DEFAULT_NAMESPACE); - IstioHTTPMatchRequest matchRequest = new IstioHTTPMatchRequest("/" + modelName - + "/" + String.valueOf(modelVersion) + "/"); + // model resource name is service name + IstioHTTPDestination destination = new IstioHTTPDestination(modelResourceName); + IstioHTTPMatchRequest matchRequest = new IstioHTTPMatchRequest( + // use namespace to avoid multi tenant problems + // use model_version_id replaced model_name to avoid illegal characters and other problems + // use model_version as the identification of the model + String.format("/seldon/%s/%s/%s/", K8sUtils.getNamespace(), id, modelVersion) + ); IstioHTTPRoute httpRoute = new IstioHTTPRoute(); httpRoute.addHTTPDestination(destination); httpRoute.addHTTPMatchRequest(matchRequest); @@ -68,4 +76,20 @@ public class IstioVirtualServiceSpec { public void setHTTPRoute(IstioHTTPRoute istioHTTPRoute) { this.httpRoute.add(istioHTTPRoute); } + + public void setHosts(List<String> hosts) { + this.hosts = hosts; + } + + public void setGateways(List<String> gateways) { + this.gateways = gateways; + } + + public List<IstioHTTPRoute> getHttpRoute() { + return httpRoute; + } + + public void setHttpRoute(List<IstioHTTPRoute> httpRoute) { + this.httpRoute = httpRoute; + } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/PredictorAnnotations.java similarity index 52% copy from submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java copy to submarine-serve/src/main/java/org/apache/submarine/serve/seldon/PredictorAnnotations.java index 48e52275..472467fc 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/PredictorAnnotations.java @@ -16,49 +16,40 @@ * specific language governing permissions and limitations * under the License. */ + package org.apache.submarine.serve.seldon; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; -public class SeldonPredictor { - @SerializedName("name") - private String name = "default"; - - @SerializedName("replicas") - private Integer replicas = 1; - - @SerializedName("graph") - private SeldonGraph seldonGraph = new SeldonGraph(); - - public SeldonPredictor(String name, Integer replicas, SeldonGraph graph) { - setName(name); - setReplicas(replicas); - setSeldonGraph(graph); - } - - public SeldonPredictor(){}; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getReplicas() { - return replicas; - } - - public void setReplicas(Integer replicas) { - this.replicas = replicas; +/** + * SeldonDeployment.spec.predictors[*].annotations + */ +public class PredictorAnnotations { + + /** + * <a href="https://docs.seldon.io/projects/seldon-core/en/latest/graph/custom_svc_name.html"> + * Model with Custom Service Name Annotations + * </a> + */ + @SerializedName("seldon.io/svc-name") + @JsonProperty("seldon.io/svc-name") + private String serviceName; + + /** + * Get predictor annotations with custom service name + */ + public static PredictorAnnotations service(String serviceName) { + return new PredictorAnnotations() + .setServiceName(serviceName); } - public SeldonGraph getSeldonGraph() { - return seldonGraph; + public String getServiceName() { + return serviceName; } - public void setSeldonGraph(SeldonGraph seldonGraph) { - this.seldonGraph = seldonGraph; + public PredictorAnnotations setServiceName(String serviceName) { + this.serviceName = serviceName; + return this; } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonDeployment.java b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonDeployment.java index 6f9a6109..b4a091e4 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonDeployment.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonDeployment.java @@ -18,15 +18,20 @@ */ package org.apache.submarine.serve.seldon; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.gson.annotations.SerializedName; import io.kubernetes.client.common.KubernetesObject; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; import org.apache.submarine.serve.utils.SeldonConstants; - -import java.util.ArrayList; -import java.util.List; +import org.apache.submarine.server.k8s.utils.K8sUtils; public class SeldonDeployment implements KubernetesObject { + + public static final String MODEL_NAME_LABEL = "model-name"; + public static final String MODEL_ID_LABEL = "model-id"; + public static final String MODEL_VERSION_LABEL = "model-version"; + @SerializedName("apiVersion") private String apiVersion = SeldonConstants.API_VERSION; @@ -39,6 +44,93 @@ public class SeldonDeployment implements KubernetesObject { @SerializedName("spec") private SeldonDeploymentSpec spec; + @JsonIgnore + private Long id; + + @JsonIgnore + private String resourceName; + + @JsonIgnore + private String modelName; + + @JsonIgnore + private String modelURI; + + @JsonIgnore + private Integer modelVersion; + + @JsonIgnore + private String modelId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getResourceName() { + return resourceName; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + public String getModelName() { + return modelName; + } + + public void setModelName(String modelName) { + this.modelName = modelName; + } + + public String getModelURI() { + return modelURI; + } + + public void setModelURI(String modelURI) { + this.modelURI = modelURI; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + + public Integer getModelVersion() { + return modelVersion; + } + + public void setModelVersion(Integer modelVersion) { + this.modelVersion = modelVersion; + } + + public SeldonDeployment() { + } + + public SeldonDeployment(Long id, String resourceName, String modelName, Integer modelVersion, + String modelId, String modelURI) { + this.id = id; + this.resourceName = resourceName; + this.modelName = modelName; + this.modelVersion = modelVersion; + this.modelId = modelId; + this.modelURI = modelURI; + + V1ObjectMetaBuilder metaBuilder = new V1ObjectMetaBuilder(); + metaBuilder.withNamespace(K8sUtils.getNamespace()) + .withName(resourceName) + .addToLabels(MODEL_NAME_LABEL, modelName) + .addToLabels(MODEL_ID_LABEL, modelId) + .addToLabels(MODEL_VERSION_LABEL, String.valueOf(modelVersion)); + setMetadata(metaBuilder.build()); + } + // transient to avoid being serialized private transient String group = SeldonConstants.GROUP; @@ -98,30 +190,11 @@ public class SeldonDeployment implements KubernetesObject { this.spec = seldonDeploymentSpec; } - public void addPredictor(SeldonPredictor seldonPredictor) { - this.spec.addPredictor(seldonPredictor); + public SeldonDeploymentSpec getSpec() { + return spec; } - public static class SeldonDeploymentSpec { - public SeldonDeploymentSpec(String protocol) { - setProtocol(protocol); - } - - @SerializedName("protocol") - private String protocol; - @SerializedName("predictors") - private List<SeldonPredictor> predictors = new ArrayList<>(); - - public String getProtocol() { - return protocol; - } - - public void setProtocol(String protocol) { - this.protocol = protocol; - } - - public void addPredictor(SeldonPredictor seldonPredictor) { - predictors.add(seldonPredictor); - } + public void addPredictor(SeldonPredictor seldonPredictor) { + this.spec.addPredictor(seldonPredictor); } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonDeploymentSpec.java similarity index 53% copy from submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java copy to submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonDeploymentSpec.java index 48e52275..016d063b 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonDeploymentSpec.java @@ -16,49 +16,46 @@ * specific language governing permissions and limitations * under the License. */ + package org.apache.submarine.serve.seldon; import com.google.gson.annotations.SerializedName; -public class SeldonPredictor { - @SerializedName("name") - private String name = "default"; +import java.util.ArrayList; +import java.util.List; - @SerializedName("replicas") - private Integer replicas = 1; +public class SeldonDeploymentSpec { - @SerializedName("graph") - private SeldonGraph seldonGraph = new SeldonGraph(); + public SeldonDeploymentSpec() { + } - public SeldonPredictor(String name, Integer replicas, SeldonGraph graph) { - setName(name); - setReplicas(replicas); - setSeldonGraph(graph); + public SeldonDeploymentSpec(String protocol) { + setProtocol(protocol); } - public SeldonPredictor(){}; + @SerializedName("protocol") + private String protocol; - public String getName() { - return name; - } + @SerializedName("predictors") + private List<SeldonPredictor> predictors = new ArrayList<>(); - public void setName(String name) { - this.name = name; + public String getProtocol() { + return protocol; } - public Integer getReplicas() { - return replicas; + public void setProtocol(String protocol) { + this.protocol = protocol; } - public void setReplicas(Integer replicas) { - this.replicas = replicas; + public void addPredictor(SeldonPredictor seldonPredictor) { + predictors.add(seldonPredictor); } - public SeldonGraph getSeldonGraph() { - return seldonGraph; + public List<SeldonPredictor> getPredictors() { + return predictors; } - public void setSeldonGraph(SeldonGraph seldonGraph) { - this.seldonGraph = seldonGraph; + public void setPredictors(List<SeldonPredictor> predictors) { + this.predictors = predictors; } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonGraph.java b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonGraph.java index 48b7e25b..d38956b0 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonGraph.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonGraph.java @@ -33,6 +33,9 @@ public class SeldonGraph { @SerializedName("envSecretRefName") private String envSecretRefName = SeldonConstants.ENV_SECRET_REF_NAME; + public SeldonGraph() { + } + public String getName() { return name; } @@ -56,4 +59,20 @@ public class SeldonGraph { public void setModelUri(String modelUri) { this.modelUri = modelUri; } + + public String getStorageInitializerImage() { + return storageInitializerImage; + } + + public void setStorageInitializerImage(String storageInitializerImage) { + this.storageInitializerImage = storageInitializerImage; + } + + public String getEnvSecretRefName() { + return envSecretRefName; + } + + public void setEnvSecretRefName(String envSecretRefName) { + this.envSecretRefName = envSecretRefName; + } } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java index 48e52275..56347f6e 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/SeldonPredictor.java @@ -18,9 +18,11 @@ */ package org.apache.submarine.serve.seldon; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; public class SeldonPredictor { + @SerializedName("name") private String name = "default"; @@ -28,8 +30,12 @@ public class SeldonPredictor { private Integer replicas = 1; @SerializedName("graph") + @JsonProperty("graph") private SeldonGraph seldonGraph = new SeldonGraph(); + @SerializedName("annotations") + private PredictorAnnotations annotations; + public SeldonPredictor(String name, Integer replicas, SeldonGraph graph) { setName(name); setReplicas(replicas); @@ -61,4 +67,13 @@ public class SeldonPredictor { public void setSeldonGraph(SeldonGraph seldonGraph) { this.seldonGraph = seldonGraph; } + + public PredictorAnnotations getAnnotations() { + return annotations; + } + + public void setAnnotations(PredictorAnnotations annotations) { + this.annotations = annotations; + } + } diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/pytorch/SeldonPytorchServing.java b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/pytorch/SeldonPytorchServing.java similarity index 71% rename from submarine-serve/src/main/java/org/apache/submarine/serve/pytorch/SeldonPytorchServing.java rename to submarine-serve/src/main/java/org/apache/submarine/serve/seldon/pytorch/SeldonPytorchServing.java index 3993ee7a..81e2844b 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/pytorch/SeldonPytorchServing.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/pytorch/SeldonPytorchServing.java @@ -16,28 +16,32 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.submarine.serve.pytorch; +package org.apache.submarine.serve.seldon.pytorch; -import io.kubernetes.client.openapi.models.V1ObjectMeta; +import org.apache.submarine.serve.seldon.PredictorAnnotations; import org.apache.submarine.serve.seldon.SeldonDeployment; +import org.apache.submarine.serve.seldon.SeldonDeploymentSpec; import org.apache.submarine.serve.seldon.SeldonGraph; import org.apache.submarine.serve.seldon.SeldonPredictor; import org.apache.submarine.serve.utils.SeldonConstants; public class SeldonPytorchServing extends SeldonDeployment { - public SeldonPytorchServing(String name, String modelURI){ - V1ObjectMeta v1ObjectMeta = new V1ObjectMeta(); - v1ObjectMeta.setName(name); - v1ObjectMeta.setNamespace(SeldonConstants.DEFAULT_NAMESPACE); - setMetadata(v1ObjectMeta); + + public SeldonPytorchServing() { + } + + public SeldonPytorchServing(Long id, String resourceName, String modelName, Integer modelVersion, + String modelId, String modelURI) { + super(id, resourceName, modelName, modelVersion, modelId, modelURI); setSpec(new SeldonDeploymentSpec(SeldonConstants.KFSERVING_PROTOCOL)); SeldonGraph seldonGraph = new SeldonGraph(); - seldonGraph.setName(name); + seldonGraph.setName(String.format("version-%s", modelVersion)); seldonGraph.setImplementation(SeldonConstants.TRITON_IMPLEMENTATION); seldonGraph.setModelUri(modelURI); SeldonPredictor seldonPredictor = new SeldonPredictor(); + seldonPredictor.setAnnotations(PredictorAnnotations.service(resourceName)); seldonPredictor.setSeldonGraph(seldonGraph); addPredictor(seldonPredictor); diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/tensorflow/SeldonTFServing.java b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/tensorflow/SeldonTFServing.java similarity index 71% rename from submarine-serve/src/main/java/org/apache/submarine/serve/tensorflow/SeldonTFServing.java rename to submarine-serve/src/main/java/org/apache/submarine/serve/seldon/tensorflow/SeldonTFServing.java index 91565fd8..ebea68ec 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/tensorflow/SeldonTFServing.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/seldon/tensorflow/SeldonTFServing.java @@ -16,28 +16,32 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.submarine.serve.tensorflow; +package org.apache.submarine.serve.seldon.tensorflow; -import io.kubernetes.client.openapi.models.V1ObjectMeta; +import org.apache.submarine.serve.seldon.PredictorAnnotations; import org.apache.submarine.serve.seldon.SeldonDeployment; +import org.apache.submarine.serve.seldon.SeldonDeploymentSpec; import org.apache.submarine.serve.seldon.SeldonGraph; import org.apache.submarine.serve.seldon.SeldonPredictor; import org.apache.submarine.serve.utils.SeldonConstants; public class SeldonTFServing extends SeldonDeployment { - public SeldonTFServing(String name, String modelURI){ - V1ObjectMeta v1ObjectMeta = new V1ObjectMeta(); - v1ObjectMeta.setName(name); - v1ObjectMeta.setNamespace(SeldonConstants.DEFAULT_NAMESPACE); - setMetadata(v1ObjectMeta); + + public SeldonTFServing() { + } + + public SeldonTFServing(Long id, String resourceName, String modelName, Integer modelVersion, + String modelId, String modelURI) { + super(id, resourceName, modelName, modelVersion, modelId, modelURI); setSpec(new SeldonDeploymentSpec(SeldonConstants.SELDON_PROTOCOL)); SeldonGraph seldonGraph = new SeldonGraph(); - seldonGraph.setName(name); + seldonGraph.setName(String.format("version-%s", modelVersion)); seldonGraph.setImplementation(SeldonConstants.TFSERVING_IMPLEMENTATION); seldonGraph.setModelUri(modelURI); SeldonPredictor seldonPredictor = new SeldonPredictor(); + seldonPredictor.setAnnotations(PredictorAnnotations.service(resourceName)); seldonPredictor.setSeldonGraph(seldonGraph); addPredictor(seldonPredictor); diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/utils/SeldonConstants.java b/submarine-serve/src/main/java/org/apache/submarine/serve/utils/SeldonConstants.java index e41a5646..e54d621a 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/utils/SeldonConstants.java +++ b/submarine-serve/src/main/java/org/apache/submarine/serve/utils/SeldonConstants.java @@ -33,8 +33,6 @@ public class SeldonConstants { public static final String ENV_SECRET_REF_NAME = "submarine-serve-secret"; - public static final String DEFAULT_NAMESPACE = "default"; - public static final String SELDON_PROTOCOL = "seldon"; public static final String KFSERVING_PROTOCOL = "kfserving"; diff --git a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/model/ServeSpec.java b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/model/ServeSpec.java index 5fa11ad0..10ca17d2 100644 --- a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/model/ServeSpec.java +++ b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/model/ServeSpec.java @@ -19,12 +19,22 @@ package org.apache.submarine.server.api.model; public class ServeSpec { + + private Long id; private String modelName; private Integer modelVersion; private String modelId; private String modelType; private String modelURI; + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + public String getModelName() { return modelName; } diff --git a/submarine-server/server-core/pom.xml b/submarine-server/server-core/pom.xml index d33ff945..311ee5a6 100644 --- a/submarine-server/server-core/pom.xml +++ b/submarine-server/server-core/pom.xml @@ -343,9 +343,19 @@ <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> + <exclusion> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </exclusion> </exclusions> <scope>test</scope> </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>${commons-codec.version}</version> + <scope>test</scope> + </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> diff --git a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/ModelVersionEntity.java b/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/ModelVersionEntity.java index e5a945b5..90da814a 100644 --- a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/ModelVersionEntity.java +++ b/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/ModelVersionEntity.java @@ -23,6 +23,9 @@ import java.sql.Timestamp; import java.util.List; public class ModelVersionEntity { + + private Long modelVersionId; + private String name; private Integer version; @@ -47,6 +50,14 @@ public class ModelVersionEntity { private List<String> tags; + public Long getModelVersionId() { + return modelVersionId; + } + + public void setModelVersionId(Long modelVersionId) { + this.modelVersionId = modelVersionId; + } + public String getName() { return name; } @@ -147,7 +158,8 @@ public class ModelVersionEntity { public String toString() { return "ModelVersionEntity{" + - "name='" + name + '\'' + + "modelVersionId=" + modelVersionId + + ", name='" + name + '\'' + ", version='" + version + '\'' + ", id='" + id + '\'' + ", userId='" + userId + '\'' + diff --git a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/RegisteredModelEntity.java b/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/RegisteredModelEntity.java index e52cac7d..a18c1a18 100644 --- a/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/RegisteredModelEntity.java +++ b/submarine-server/server-database/src/main/java/org/apache/submarine/server/database/model/entities/RegisteredModelEntity.java @@ -24,6 +24,8 @@ import java.util.List; public class RegisteredModelEntity { + private Long modelId; + private String name; private Timestamp creationTime; @@ -34,6 +36,14 @@ public class RegisteredModelEntity { private List<String> tags; + public Long getModelId() { + return modelId; + } + + public void setModelId(Long modelId) { + this.modelId = modelId; + } + public String getName() { return name; } @@ -78,7 +88,8 @@ public class RegisteredModelEntity { public String toString() { return "RegisteredModelEntity{" + - "name='" + name + '\'' + + "modelId=" + modelId + + ", name='" + name + '\'' + ", createTime='" + creationTime + '\'' + ", lastUpdatedTime=" + lastUpdatedTime + '\'' + ", description='" + description + '\'' + diff --git a/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/ModelVersionMapper.xml b/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/ModelVersionMapper.xml index e16e95ae..d868e857 100644 --- a/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/ModelVersionMapper.xml +++ b/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/ModelVersionMapper.xml @@ -20,6 +20,7 @@ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.apache.submarine.server.database.model.mappers.ModelVersionMapper"> <resultMap id="resultMap" type="org.apache.submarine.server.database.model.entities.ModelVersionEntity"> + <result column="model_version_id" property="modelVersionId" /> <result column="name" property="name" /> <result column="version" property="version" /> <result column="id" property="id" /> @@ -34,6 +35,7 @@ </resultMap> <resultMap id="resultMapWithTag" type="org.apache.submarine.server.database.model.entities.ModelVersionEntity"> + <result column="model_version_id" property="modelVersionId" /> <result column="name" property="name" /> <result column="version" property="version" /> <result column="id" property="id" /> @@ -51,7 +53,7 @@ </resultMap> <sql id="Base_Column_List"> - name, version, id, user_id, experiment_id, model_type, current_stage, creation_time, + model_version_id, name, version, id, user_id, experiment_id, model_type, current_stage, creation_time, last_updated_time, dataset, description </sql> @@ -76,7 +78,7 @@ where mv.name = #{name,jdbcType=VARCHAR} </select> - <insert id="insert" parameterType="org.apache.submarine.server.database.model.entities.ModelVersionEntity"> + <insert id="insert" parameterType="org.apache.submarine.server.database.model.entities.ModelVersionEntity" useGeneratedKeys="true" keyProperty="modelVersionId"> insert into model_version (name, version, id, user_id, experiment_id, model_type, current_stage, creation_time, last_updated_time, dataset, description) values (#{name,jdbcType=VARCHAR}, #{version,jdbcType=INTEGER}, #{id,jdbcType=VARCHAR}, #{userId,jdbcType=VARCHAR}, #{experimentId,jdbcType=VARCHAR}, #{modelType,jdbcType=VARCHAR}, #{currentStage,jdbcType=VARCHAR}, diff --git a/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/RegisteredModelMapper.xml b/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/RegisteredModelMapper.xml index eb165858..dd858569 100644 --- a/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/RegisteredModelMapper.xml +++ b/submarine-server/server-database/src/main/resources/org/apache/submarine/database/mappers/RegisteredModelMapper.xml @@ -25,6 +25,7 @@ </resultMap> <resultMap id="resultMap" type="org.apache.submarine.server.database.model.entities.RegisteredModelEntity"> + <result column="model_id" property="modelId" /> <result column="name" property="name" /> <result column="creation_time" property="creationTime" /> <result column="last_updated_time" property="lastUpdatedTime" /> @@ -32,6 +33,7 @@ </resultMap> <resultMap id="resultMapWithTag" type="org.apache.submarine.server.database.model.entities.RegisteredModelEntity"> + <result column="model_id" property="modelId" /> <result column="name" property="name" /> <result column="creation_time" property="creationTime" /> <result column="last_updated_time" property="lastUpdatedTime" /> @@ -42,7 +44,7 @@ </resultMap> <sql id="Base_Column_List"> - name, creation_time, last_updated_time, description + model_id, name, creation_time, last_updated_time, description </sql> <select id="select" parameterType="java.lang.String" resultMap="resultMap"> @@ -63,7 +65,7 @@ from registered_model rm left join registered_model_tag tag on tag.name = rm.name </select> - <insert id="insert" parameterType="org.apache.submarine.server.database.model.entities.RegisteredModelEntity"> + <insert id="insert" parameterType="org.apache.submarine.server.database.model.entities.RegisteredModelEntity" useGeneratedKeys="true" keyProperty="id"> insert into registered_model (name, creation_time, last_updated_time, description) values (#{name,jdbcType=VARCHAR}, NOW(3), NOW(3), #{description,jdbcType=VARCHAR}); <if test="tags != null and !tags.isEmpty()"> diff --git a/submarine-server/server-submitter/submitter-k8s/pom.xml b/submarine-server/server-submitter/submitter-k8s/pom.xml index 96e518b4..810090b2 100644 --- a/submarine-server/server-submitter/submitter-k8s/pom.xml +++ b/submarine-server/server-submitter/submitter-k8s/pom.xml @@ -100,11 +100,11 @@ <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> + <dependency> <groupId>org.apache.submarine</groupId> <artifactId>submarine-serve</artifactId> <version>${project.version}</version> - <scope>compile</scope> </dependency> <dependency> diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java index a550cec1..755298c7 100644 --- a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java +++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java @@ -31,7 +31,6 @@ import io.kubernetes.client.openapi.models.V1Deployment; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Pod; import io.kubernetes.client.openapi.models.V1PodList; -import io.kubernetes.client.util.generic.options.CreateOptions; import io.kubernetes.client.util.generic.options.DeleteOptions; import io.kubernetes.client.util.generic.options.ListOptions; @@ -39,9 +38,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.submarine.commons.utils.SubmarineConfVars; import org.apache.submarine.commons.utils.SubmarineConfiguration; import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException; -import org.apache.submarine.serve.pytorch.SeldonPytorchServing; -import org.apache.submarine.serve.seldon.SeldonDeployment; -import org.apache.submarine.serve.tensorflow.SeldonTFServing; import org.apache.submarine.server.k8s.utils.K8sUtils; import org.apache.submarine.server.api.Submitter; import org.apache.submarine.server.api.common.CustomResourceType; @@ -66,6 +62,8 @@ import org.apache.submarine.server.submitter.k8s.model.common.PersistentVolumeCl import org.apache.submarine.server.submitter.k8s.model.mljob.MLJob; import org.apache.submarine.server.submitter.k8s.model.mljob.MLJobFactory; import org.apache.submarine.server.submitter.k8s.model.notebook.NotebookCR; +import org.apache.submarine.server.submitter.k8s.model.seldon.SeldonDeploymentFactory; +import org.apache.submarine.server.submitter.k8s.model.seldon.SeldonResource; import org.apache.submarine.server.submitter.k8s.util.NotebookUtils; import org.apache.submarine.server.submitter.k8s.util.OwnerReferenceUtils; @@ -423,48 +421,23 @@ public class K8sSubmitter implements Submitter { } @Override - public void createServe(ServeSpec spec) - throws SubmarineRuntimeException { - SeldonDeployment seldonDeployment = parseServeSpec(spec); - IstioVirtualService istioVirtualService = new IstioVirtualService(spec.getModelName(), - spec.getModelVersion()); - try { - k8sClient.getSeldonDeploymentClient().create("default", seldonDeployment, - new CreateOptions()).throwsApiException(); - } catch (ApiException e) { - LOG.error(e.getMessage(), e); - throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); - } - try { - k8sClient.getIstioVirtualServiceClient().create("default", istioVirtualService, new CreateOptions()) - .throwsApiException(); - } catch (ApiException e) { - LOG.error(e.getMessage(), e); - try { - k8sClient.getSeldonDeploymentClient().delete("default", seldonDeployment.getMetadata().getName(), - getDeleteOptions(seldonDeployment.getApiVersion())).throwsApiException(); - } catch (ApiException e1) { - LOG.error(e1.getMessage(), e1); - } - throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); - } + public void createServe(ServeSpec spec) throws SubmarineRuntimeException { + // Seldon Deployment Resource + SeldonResource seldonDeployment = SeldonDeploymentFactory.getSeldonDeployment(spec); + // VirtualService Resource + IstioVirtualService istioVirtualService = seldonDeployment.getIstioVirtualService(); + // commit SeldonResource and IstioVirtualService with transaction + resourceTransaction(seldonDeployment, istioVirtualService); } @Override - public void deleteServe(ServeSpec spec) - throws SubmarineRuntimeException { - SeldonDeployment seldonDeployment = parseServeSpec(spec); - IstioVirtualService istioVirtualService = new IstioVirtualService(spec.getModelName(), - spec.getModelVersion()); - try { - k8sClient.getSeldonDeploymentClient().delete("default", seldonDeployment.getMetadata().getName(), - getDeleteOptions(seldonDeployment.getApiVersion())).throwsApiException(); - k8sClient.getIstioVirtualServiceClient().delete("default", istioVirtualService.getMetadata().getName(), - getDeleteOptions(istioVirtualService.getApiVersion())).throwsApiException(); - } catch (ApiException e) { - LOG.error(e.getMessage(), e); - throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); - } + public void deleteServe(ServeSpec spec) throws SubmarineRuntimeException { + // Seldon Deployment Resource + SeldonResource seldonDeployment = SeldonDeploymentFactory.getSeldonDeployment(spec); + // VirtualService Resource + IstioVirtualService istioVirtualService = seldonDeployment.getIstioVirtualService(); + // Delete SeldonResource and IstioVirtualService with transaction + deleteResourcesTransaction(seldonDeployment, istioVirtualService); } private String getJobLabelSelector(ExperimentSpec experimentSpec) { @@ -480,20 +453,4 @@ public class K8sSubmitter implements Submitter { } } - private SeldonDeployment parseServeSpec(ServeSpec spec) throws SubmarineRuntimeException { - String modelName = spec.getModelName(); - String modelType = spec.getModelType(); - String modelURI = spec.getModelURI(); - - SeldonDeployment seldonDeployment; - if (modelType.equals("tensorflow")){ - seldonDeployment = new SeldonTFServing(modelName, modelURI); - } else if (modelType.equals("pytorch")){ - seldonDeployment = new SeldonPytorchServing(modelName, modelURI); - } else { - throw new SubmarineRuntimeException("Given serve type: " + modelType + " is not supported."); - } - return seldonDeployment; - } - } diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/istio/IstioVirtualService.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/istio/IstioVirtualService.java index 50eb9ebf..6a35cffd 100644 --- a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/istio/IstioVirtualService.java +++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/istio/IstioVirtualService.java @@ -44,8 +44,8 @@ public class IstioVirtualService extends org.apache.submarine.serve.istio.IstioV super(metadata, spec); } - public IstioVirtualService(String modelName, Integer modelVersion) { - super(modelName, modelVersion); + public IstioVirtualService(Long id, String modelResourceName, Integer modelVersion) { + super(id, modelResourceName, modelVersion); } public IstioVirtualService(V1ObjectMeta metadata) { @@ -72,7 +72,7 @@ public class IstioVirtualService extends org.apache.submarine.serve.istio.IstioV return this; } catch (ApiException e) { LOG.error("K8s submitter: Create notebook VirtualService custom resource object failed by " + - e.getMessage(), e); + e.getMessage(), e); throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); } catch (JsonSyntaxException e) { LOG.error("K8s submitter: parse response object failed by " + e.getMessage(), e); @@ -90,7 +90,7 @@ public class IstioVirtualService extends org.apache.submarine.serve.istio.IstioV try { if (LOG.isDebugEnabled()) { LOG.debug("Delete VirtualService resource in namespace: {} and name: {}", - this.getMetadata().getNamespace(), this.getMetadata().getName()); + this.getMetadata().getNamespace(), this.getMetadata().getName()); } api.getIstioVirtualServiceClient() .delete( @@ -100,7 +100,7 @@ public class IstioVirtualService extends org.apache.submarine.serve.istio.IstioV ).throwsApiException(); } catch (ApiException e) { LOG.error("K8s submitter: Delete notebook VirtualService custom resource object failed by " + - e.getMessage(), e); + e.getMessage(), e); K8sSubmitter.API_EXCEPTION_404_CONSUMER.apply(e); } return this; diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentFactory.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentFactory.java new file mode 100644 index 00000000..fab414fe --- /dev/null +++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentFactory.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.submarine.server.submitter.k8s.model.seldon; + +import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException; +import org.apache.submarine.server.api.model.ServeSpec; + +/** + * SeldonDeployment K8s Model Resource Factory + */ +public class SeldonDeploymentFactory { + + /** + * Get SeldonDeployment by model type + */ + public static SeldonResource getSeldonDeployment(ServeSpec spec) throws SubmarineRuntimeException { + Long id = spec.getId(); + String modelId = spec.getModelId(); + String resourceName = String.format("submarine-model-%s-%s", spec.getId(), modelId); + + String modelName = spec.getModelName(); + String modelType = spec.getModelType(); + String modelURI = spec.getModelURI(); + Integer modelVersion = spec.getModelVersion(); + + switch (modelType) { + case "tensorflow": + return new SeldonDeploymentTFServing(id, resourceName, modelName, modelVersion, + modelId, modelURI); + case "pytorch": + return new SeldonDeploymentPytorchServing(id, resourceName, modelName, modelVersion, + modelId, modelURI); + case "xgboost":// TODO(cdmikechen): Will fix https://issues.apache.org/jira/browse/SUBMARINE-1316 + default: + throw new SubmarineRuntimeException("Given serve type: " + modelType + " is not supported."); + } + } +} diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentPytorchServing.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentPytorchServing.java new file mode 100644 index 00000000..4082cc3c --- /dev/null +++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentPytorchServing.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.submarine.server.submitter.k8s.model.seldon; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.options.CreateOptions; +import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException; +import org.apache.submarine.serve.seldon.pytorch.SeldonPytorchServing; +import org.apache.submarine.serve.seldon.SeldonDeployment; +import org.apache.submarine.server.submitter.k8s.client.K8sClient; +import org.apache.submarine.server.submitter.k8s.model.istio.IstioVirtualService; +import org.apache.submarine.server.submitter.k8s.util.OwnerReferenceUtils; +import org.apache.submarine.server.submitter.k8s.util.YamlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.submarine.server.submitter.k8s.K8sSubmitter.getDeleteOptions; + +/** + * Seldon Deployment Pytorch Serving Resource + */ +public class SeldonDeploymentPytorchServing extends SeldonPytorchServing implements SeldonResource { + + private static final Logger LOG = LoggerFactory.getLogger(SeldonDeploymentPytorchServing.class); + + public SeldonDeploymentPytorchServing() { + } + + public SeldonDeploymentPytorchServing(Long id, String resourceName, String modelName, Integer modelVersion, + String modelId, String modelURI) { + super(id, resourceName, modelName, modelVersion, modelId, modelURI); + // add owner reference so that we can automatically delete it when submarine CR has been deleted + getMetadata().setOwnerReferences(OwnerReferenceUtils.getOwnerReference()); + } + + @Override + public SeldonDeployment read(K8sClient api) { + throw new UnsupportedOperationException(); + } + + @Override + public SeldonDeployment create(K8sClient api) { + try { + if (LOG.isDebugEnabled()) { + LOG.debug("Create Seldon PytorchServing resource: \n{}", YamlUtils.toPrettyYaml(this)); + } + api.getSeldonDeploymentClient() + .create(getMetadata().getNamespace(), this, new CreateOptions()) + .throwsApiException(); + return this; + } catch (ApiException e) { + LOG.error(e.getMessage(), e); + throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); + } + } + + @Override + public SeldonDeployment replace(K8sClient api) { + throw new UnsupportedOperationException(); + } + + @Override + public SeldonDeployment delete(K8sClient api) { + try { + if (LOG.isDebugEnabled()) { + LOG.debug("Delete Seldon PytorchServing resource in namespace: {} and name: {}", + this.getMetadata().getNamespace(), this.getMetadata().getName()); + } + api.getSeldonDeploymentClient() + .delete(getMetadata().getNamespace(), getMetadata().getName(), + getDeleteOptions(getApiVersion())) + .throwsApiException(); + return this; + } catch (ApiException e) { + LOG.error(e.getMessage(), e); + throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); + } + } + + @Override + public IstioVirtualService getIstioVirtualService() { + IstioVirtualService service = new IstioVirtualService( + getId(), getMetadata().getName(), getModelVersion() + ); + service.getMetadata().putLabelsItem(MODEL_NAME_LABEL, getModelName()); + service.getMetadata().putLabelsItem(MODEL_ID_LABEL, getModelId()); + service.getMetadata().putLabelsItem(MODEL_VERSION_LABEL, String.valueOf(getModelVersion())); + return service; + } +} diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentTFServing.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentTFServing.java new file mode 100644 index 00000000..44518f25 --- /dev/null +++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonDeploymentTFServing.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.submarine.server.submitter.k8s.model.seldon; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.options.CreateOptions; +import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException; +import org.apache.submarine.serve.seldon.tensorflow.SeldonTFServing; +import org.apache.submarine.server.submitter.k8s.client.K8sClient; +import org.apache.submarine.server.submitter.k8s.model.istio.IstioVirtualService; +import org.apache.submarine.server.submitter.k8s.util.OwnerReferenceUtils; +import org.apache.submarine.server.submitter.k8s.util.YamlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.submarine.server.submitter.k8s.K8sSubmitter.getDeleteOptions; + +/** + * Seldon Deployment Tensorflow Serving Resource + */ +public class SeldonDeploymentTFServing extends SeldonTFServing implements SeldonResource { + + private static final Logger LOG = LoggerFactory.getLogger(SeldonDeploymentTFServing.class); + + public SeldonDeploymentTFServing() { + } + + public SeldonDeploymentTFServing(Long id, String resourceName, String modelName, Integer modelVersion, + String modelId, String modelURI) { + super(id, resourceName, modelName, modelVersion, modelId, modelURI); + // add owner reference so that we can automatically delete it when submarine CR has been deleted + getMetadata().setOwnerReferences(OwnerReferenceUtils.getOwnerReference()); + } + + @Override + public SeldonTFServing read(K8sClient api) { + throw new UnsupportedOperationException(); + } + + @Override + public SeldonTFServing create(K8sClient api) { + try { + if (LOG.isDebugEnabled()) { + LOG.debug("Create Seldon TFServing resource: \n{}", YamlUtils.toPrettyYaml(this)); + } + api.getSeldonDeploymentClient() + .create(getMetadata().getNamespace(), this, new CreateOptions()) + .throwsApiException(); + return this; + } catch (ApiException e) { + LOG.error(e.getMessage(), e); + throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); + } + } + + @Override + public SeldonTFServing replace(K8sClient api) { + throw new UnsupportedOperationException(); + } + + @Override + public SeldonTFServing delete(K8sClient api) { + try { + if (LOG.isDebugEnabled()) { + LOG.debug("Delete Seldon TFServing resource in namespace: {} and name: {}", + this.getMetadata().getNamespace(), this.getMetadata().getName()); + } + api.getSeldonDeploymentClient() + .delete(getMetadata().getNamespace(), getMetadata().getName(), + getDeleteOptions(getApiVersion())) + .throwsApiException(); + return this; + } catch (ApiException e) { + LOG.error(e.getMessage(), e); + throw new SubmarineRuntimeException(e.getCode(), e.getMessage()); + } + } + + @Override + public IstioVirtualService getIstioVirtualService() { + IstioVirtualService service = new IstioVirtualService( + getId(), getMetadata().getName(), getModelVersion() + ); + service.getMetadata().putLabelsItem(MODEL_NAME_LABEL, getModelName()); + service.getMetadata().putLabelsItem(MODEL_ID_LABEL, getModelId()); + service.getMetadata().putLabelsItem(MODEL_VERSION_LABEL, String.valueOf(getModelVersion())); + return service; + } +} diff --git a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPMatchRequest.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonResource.java similarity index 56% copy from submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPMatchRequest.java copy to submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonResource.java index 18e0b6c5..b304fe94 100644 --- a/submarine-serve/src/main/java/org/apache/submarine/serve/istio/IstioHTTPMatchRequest.java +++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/seldon/SeldonResource.java @@ -16,24 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.submarine.serve.istio; -import com.google.gson.annotations.SerializedName; +package org.apache.submarine.server.submitter.k8s.model.seldon; -public class IstioHTTPMatchRequest { - @SerializedName("uri") - private IstioPrefix prefix; +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.apache.submarine.serve.seldon.SeldonDeployment; +import org.apache.submarine.server.submitter.k8s.model.K8sResource; +import org.apache.submarine.server.submitter.k8s.model.istio.IstioVirtualService; - public IstioHTTPMatchRequest(String prefix) { - this.prefix = new IstioPrefix(prefix); - } +public interface SeldonResource extends K8sResource<SeldonDeployment> { - public static class IstioPrefix { - @SerializedName("prefix") - private String path; + /** + * Get IstioVirtualService, Using the name directly may result in illegal characters, + * so we need to do a conversion using the resource name of the SeldonResource + */ + @JsonIgnore + IstioVirtualService getIstioVirtualService(); - public IstioPrefix(String path){ - this.path = path; - } - } } diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/util/YamlUtils.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/util/YamlUtils.java index b50d180b..06d0ea1c 100644 --- a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/util/YamlUtils.java +++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/util/YamlUtils.java @@ -50,6 +50,9 @@ public class YamlUtils { .setSerializationInclusion(JsonInclude.Include.NON_NULL); } + /** + * Pretty yaml + */ public static String toPrettyYaml(Object pojoObject) { try { return YAML_MAPPER.writeValueAsString(pojoObject); @@ -57,4 +60,15 @@ public class YamlUtils { throw new RuntimeException("Parse yaml failed! " + pojoObject.getClass().getName(), ex); } } + + /** + * Read yaml to class + */ + public static <T> T readValue(String content, Class<T> tClass) { + try { + return YAML_MAPPER.readValue(content, tClass); + } catch (JsonProcessingException ex) { + throw new RuntimeException("Read yaml failed! " + tClass.getName(), ex); + } + } } diff --git a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/seldon/SeldonDeploymentResourceTest.java b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/seldon/SeldonDeploymentResourceTest.java new file mode 100644 index 00000000..a18697ae --- /dev/null +++ b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/seldon/SeldonDeploymentResourceTest.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.submarine.server.submitter.k8s.seldon; + +import org.apache.submarine.server.api.model.ServeSpec; +import org.apache.submarine.server.submitter.k8s.model.seldon.SeldonDeploymentFactory; +import org.apache.submarine.server.submitter.k8s.model.seldon.SeldonDeploymentPytorchServing; +import org.apache.submarine.server.submitter.k8s.model.seldon.SeldonDeploymentTFServing; +import org.apache.submarine.server.submitter.k8s.model.seldon.SeldonResource; +import org.apache.submarine.server.submitter.k8s.util.YamlUtils; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test Seldon Deployment Resource + */ +public class SeldonDeploymentResourceTest { + + @Test + public void testSeldonDeploymentTFServingResourceToYaml() { + ServeSpec serveSpec = new ServeSpec(); + serveSpec.setId(1L); + serveSpec.setModelVersion(1); + serveSpec.setModelName("test-model"); + serveSpec.setModelType("tensorflow"); + serveSpec.setModelId("5f0a43f251064fa8979660eddf04ede8"); + serveSpec.setModelURI("s3://submarine/registry/" + + "test-model-1-5f0a43f251064fa8979660eddf04ede8/test-model"); + + // to yaml + SeldonResource seldonDeployment = SeldonDeploymentFactory.getSeldonDeployment(serveSpec); + String yaml = YamlUtils.toPrettyYaml(seldonDeployment); + System.out.println(yaml); + + // cast to object + SeldonDeploymentTFServing sdtfs = YamlUtils.readValue(yaml, SeldonDeploymentTFServing.class); + Assert.assertEquals("submarine-model-1-5f0a43f251064fa8979660eddf04ede8", + sdtfs.getMetadata().getName()); + Assert.assertEquals(1, sdtfs.getSpec().getPredictors().size()); + Assert.assertEquals("seldon", sdtfs.getSpec().getProtocol()); + Assert.assertEquals("version-1", sdtfs.getSpec().getPredictors().get(0).getSeldonGraph().getName()); + } + + @Test + public void testSeldonDeploymentPytorchServingResourceToYaml() { + ServeSpec serveSpec = new ServeSpec(); + serveSpec.setId(2L); + serveSpec.setModelVersion(5); + serveSpec.setModelName("test-model"); + serveSpec.setModelType("pytorch"); + serveSpec.setModelId("5f0a43f251064fa8979660eddf04ede8"); + serveSpec.setModelURI("s3://submarine/registry/" + + "test-model-1-5f0a43f251064fa8979660eddf04ede8/test-model"); + + // to yaml + SeldonResource seldonDeployment = SeldonDeploymentFactory.getSeldonDeployment(serveSpec); + String yaml = YamlUtils.toPrettyYaml(seldonDeployment); + System.out.println(yaml); + + // cast to object + SeldonDeploymentPytorchServing sdpts = YamlUtils.readValue(yaml, SeldonDeploymentPytorchServing.class); + Assert.assertEquals("submarine-model-2-5f0a43f251064fa8979660eddf04ede8", + sdpts.getMetadata().getName()); + Assert.assertEquals("kfserving", sdpts.getSpec().getProtocol()); + Assert.assertEquals(1, sdpts.getSpec().getPredictors().size()); + Assert.assertEquals("version-5", sdpts.getSpec().getPredictors().get(0).getSeldonGraph().getName()); + } + +} diff --git a/submarine-workbench/workbench-web/src/app/interfaces/model-info.ts b/submarine-workbench/workbench-web/src/app/interfaces/model-info.ts index 7fe01b99..1a0fbd49 100644 --- a/submarine-workbench/workbench-web/src/app/interfaces/model-info.ts +++ b/submarine-workbench/workbench-web/src/app/interfaces/model-info.ts @@ -20,7 +20,7 @@ export interface ModelInfo { name: string; creationTime: string, - lastUpdatedTime: string, + lastUpdatedTime: string, tags: string[], description: string, -} \ No newline at end of file +} diff --git a/submarine-workbench/workbench-web/src/app/interfaces/model-serve.ts b/submarine-workbench/workbench-web/src/app/interfaces/model-serve.ts index 4853e3d4..8882fbdb 100644 --- a/submarine-workbench/workbench-web/src/app/interfaces/model-serve.ts +++ b/submarine-workbench/workbench-web/src/app/interfaces/model-serve.ts @@ -17,8 +17,8 @@ * under the License. */ - export interface ServeSpec { + id: number, modelName: string, modelVersion: number, -} \ No newline at end of file +} diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.html b/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.html index 1e422d29..7f87d11a 100644 --- a/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.html +++ b/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.html @@ -85,7 +85,7 @@ nzTitle="Are you sure you want to serve the model?" nzCancelText="Cancel" nzOkText="Ok" - (nzOnConfirm)="onCreateServe(data.version)" + (nzOnConfirm)="onCreateServe(data.modelVersionId, data.version)" (click)="preventEvent($event)" > <i id="icon-createServe{{ i }}" nz-icon nzType="play-circle" nzTheme="fill" class="model-info-icon" ></i> @@ -99,7 +99,7 @@ nzTitle="Are you sure you want to delete the model serve?" nzCancelText="Cancel" nzOkText="Ok" - (nzOnConfirm)="onDeleteServe(data.version)" + (nzOnConfirm)="onDeleteServe(data.modelVersionId, data.version)" (click)="preventEvent($event)" > <i id="icon-pause{{ i }}" nz-icon nzType="pause-circle" nzTheme="fill" class="model-info-icon"></i> @@ -115,7 +115,7 @@ nzCancelText="Cancel" nzOkText="Ok" (nzOnConfirm)="onDeleteModelVersion(data.version)" - (click)="preventEvent($event)" + (click)="preventEvent($event)" > <i nz-icon nzType="delete" nzTheme="fill" class="model-info-icon" ></i> </a> @@ -133,4 +133,4 @@ </tr> </tbody> </nz-table> -</div> \ No newline at end of file +</div> diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.ts index b9818066..97218f18 100644 --- a/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.ts +++ b/submarine-workbench/workbench-web/src/app/pages/workbench/model/model-info/model-info.component.ts @@ -37,15 +37,15 @@ export class ModelInfoComponent implements OnInit { isModelInfoLoading: boolean = true; isModelVersionsLoading: boolean = true; modelName: string; - selectedModelInfo: ModelInfo; + selectedModelInfo: ModelInfo; modelVersions: ModelVersionInfo[]; humanizedCreationTime: string; humanizedLastUpdatedTime: string; constructor( - private router: Router, - private route: ActivatedRoute, - private modelVersionService: ModelVersionService, + private router: Router, + private route: ActivatedRoute, + private modelVersionService: ModelVersionService, private modelService: ModelService, private modelServeService: ModelServeService, private nzMessageService: NzMessageService, @@ -78,10 +78,12 @@ export class ModelInfoComponent implements OnInit { ); } - onCreateServe = (version: number) => { - this.modelServeService.createServe(this.modelName, version).subscribe({ + onCreateServe = (id: number, version: number) => { + this.modelServeService.createServe(id, this.modelName, version).subscribe({ next: (result) => { this.nzMessageService.success(`The model serve with name: ${this.modelName} and version: ${version} is created.`) + // refresh model version status after created serve + this.fetchModelAllVersions(); }, error: (msg) => { this.nzMessageService.error(`${msg}, please try again`, { @@ -91,10 +93,12 @@ export class ModelInfoComponent implements OnInit { }) } - onDeleteServe = (version: number) => { - this.modelServeService.deleteServe(this.modelName, version).subscribe({ + onDeleteServe = (id: number, version: number) => { + this.modelServeService.deleteServe(id, this.modelName, version).subscribe({ next: (result) => { this.nzMessageService.success(`The model serve with name: ${this.modelName} and version: ${version} is deleted.`) + // refresh model version status after deleted serve + this.fetchModelAllVersions(); }, error: (msg) => { this.nzMessageService.error(`${msg}, please try again`, { diff --git a/submarine-workbench/workbench-web/src/app/services/model-serve.service.ts b/submarine-workbench/workbench-web/src/app/services/model-serve.service.ts index 4662dd12..b37cc906 100644 --- a/submarine-workbench/workbench-web/src/app/services/model-serve.service.ts +++ b/submarine-workbench/workbench-web/src/app/services/model-serve.service.ts @@ -38,9 +38,10 @@ export class ModelServeService { this.emitInfoSource.next(id); } - createServe(modelName: string, modelVersion: number) : Observable<string> { + createServe(id: number, modelName: string, modelVersion: number) : Observable<string> { const apiUrl = this.baseApi.getRestApi('/v1/serve'); const serveSpec : ServeSpec = { + id, modelName, modelVersion } @@ -64,9 +65,10 @@ export class ModelServeService { ); } - deleteServe(modelName: string, modelVersion: number) : Observable<string> { + deleteServe(id: number, modelName: string, modelVersion: number) : Observable<string> { const apiUrl = this.baseApi.getRestApi(`/v1/serve`); const serveSpec : ServeSpec = { + id, modelName, modelVersion } @@ -96,4 +98,4 @@ export class ModelServeService { ) } -} \ No newline at end of file +} --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@submarine.apache.org For additional commands, e-mail: dev-h...@submarine.apache.org