diff --git a/doc/source/graph/svcorch.md b/doc/source/graph/svcorch.md index 10aa05a4a2..48cc2048fd 100644 --- a/doc/source/graph/svcorch.md +++ b/doc/source/graph/svcorch.md @@ -4,7 +4,7 @@ The service orchestrator is a component that is added to your inference graph to * Correctly manage the request/response paths described by your inference graph * Expose prometheus metrics - + * Add meta data to the response ## Resource Requests/Limits for Service Orchetsrator @@ -67,4 +67,40 @@ The service orchestrator is a Java component. You can directly control its java You can manipulate some of the functionality of the service orchestrator by adding specific environment variables to the `svcOrchSpec` section. * [Configure Jaeger Tracing Example](../graph/distributed-tracing.html) - * [Set logging level in service orchestrator engine](../analytics/log_level.html#setting-log-level-in-the-seldon-engine) \ No newline at end of file + * [Set logging level in service orchestrator engine](../analytics/log_level.html#setting-log-level-in-the-seldon-engine) + +## Bypass Service Orchestrator (version >= 0.5.0, alpha feature) + +If you are deploying a single model then for those wishing to minimize the network hops and have lowest latency and resource usage for their deployed model you can opt out of having the service orchestrator included. To do this add the annotation `seldon.io/no-engine: "true"` to the predictor. The predictor must contain just a single node graph. An example is shown below: + + +```YAML +apiVersion: machinelearning.seldon.io/v1alpha2 +kind: SeldonDeployment +metadata: + labels: + app: seldon + name: noengine +spec: + name: noeng + predictors: + - annotations: + seldon.io/no-engine: "true" + componentSpecs: + - spec: + containers: + - image: seldonio/mock_classifier_rest:1.3 + name: classifier + graph: + children: [] + endpoint: + type: REST + name: classifier + type: MODEL + name: noeng + replicas: 1 +``` + +In these cases the external API requests will be sent directly to your model. At present only the python wrapper (>=0.13-SNAPSHOT) has been modified to allow this. + +Note no metrics or extra data will be added to the request so this would need to be done by your model itself if needed. diff --git a/examples/models/mean_classifier/Makefile b/examples/models/mean_classifier/Makefile index 5941c97b77..48f56c14f1 100644 --- a/examples/models/mean_classifier/Makefile +++ b/examples/models/mean_classifier/Makefile @@ -2,13 +2,13 @@ VERSION=1.3 IMAGE_BASE=seldonio/mock_classifier build_rest: - s2i build -E environment_rest . seldonio/seldon-core-s2i-python36:0.11-SNAPSHOT ${IMAGE_BASE}_rest:${VERSION} + s2i build -E environment_rest . seldonio/seldon-core-s2i-python36:0.13-SNAPSHOT ${IMAGE_BASE}_rest:${VERSION} push_rest: docker push ${IMAGE_BASE}_rest:${VERSION} build_grpc: - s2i build -E environment_grpc . seldonio/seldon-core-s2i-python36:0.11-SNAPSHOT ${IMAGE_BASE}_grpc:${VERSION} + s2i build -E environment_grpc . seldonio/seldon-core-s2i-python36:0.13-SNAPSHOT ${IMAGE_BASE}_grpc:${VERSION} push_grpc: docker push ${IMAGE_BASE}_grpc:${VERSION} diff --git a/notebooks/cat-raw.jpg b/notebooks/cat-raw.jpg new file mode 100644 index 0000000000..1d9b9e4d3a Binary files /dev/null and b/notebooks/cat-raw.jpg differ diff --git a/notebooks/resources/model_no_engine.yaml b/notebooks/resources/model_no_engine.yaml new file mode 100644 index 0000000000..a34246b447 --- /dev/null +++ b/notebooks/resources/model_no_engine.yaml @@ -0,0 +1,25 @@ +apiVersion: machinelearning.seldon.io/v1alpha2 +kind: SeldonDeployment +metadata: + labels: + app: seldon + name: noengine +spec: + name: noeng + predictors: + - annotations: + seldon.io/no-engine: "true" + componentSpecs: + - spec: + containers: + - image: seldonio/mock_classifier_rest:1.3 + imagePullPolicy: Never + name: classifier + graph: + children: [] + endpoint: + type: REST + name: classifier + type: MODEL + name: noeng + replicas: 1 diff --git a/operator/api/v1alpha2/seldondeployment_types.go b/operator/api/v1alpha2/seldondeployment_types.go index 939fea98a6..4f1426c62a 100644 --- a/operator/api/v1alpha2/seldondeployment_types.go +++ b/operator/api/v1alpha2/seldondeployment_types.go @@ -43,6 +43,7 @@ const ( ANNOTATION_JAVA_OPTS = "seldon.io/engine-java-opts" ANNOTATION_SEPARATE_ENGINE = "seldon.io/engine-separate-pod" ANNOTATION_HEADLESS_SVC = "seldon.io/headless-svc" + ANNOTATION_NO_ENGINE = "seldon.io/no-engine" ANNOTATION_CUSTOM_SVC_NAME = "seldon.io/svc-name" ) diff --git a/operator/api/v1alpha2/seldondeployment_webhook.go b/operator/api/v1alpha2/seldondeployment_webhook.go index 1ae2fa0846..eddbd2c56c 100644 --- a/operator/api/v1alpha2/seldondeployment_webhook.go +++ b/operator/api/v1alpha2/seldondeployment_webhook.go @@ -398,11 +398,26 @@ func checkTraffic(mlDep *SeldonDeployment, fldPath *field.Path, allErrs field.Er return allErrs } +func sizeOfGraph(p *PredictiveUnit) int { + count := 0 + for _, child := range p.Children { + count = count + sizeOfGraph(&child) + } + return count + 1 +} + func (r *SeldonDeployment) validateSeldonDeployment() error { var allErrs field.ErrorList predictorNames := make(map[string]bool) for i, p := range r.Spec.Predictors { + + _, noEngine := p.Annotations[ANNOTATION_NO_ENGINE] + if noEngine && sizeOfGraph(p.Graph) > 1 { + fldPath := field.NewPath("spec").Child("predictors").Index(i) + allErrs = append(allErrs, field.Invalid(fldPath, p.Name, "Running without engine only valid for single element graphs")) + } + if _, present := predictorNames[p.Name]; present { fldPath := field.NewPath("spec").Child("predictors").Index(i) allErrs = append(allErrs, field.Invalid(fldPath, p.Name, "Duplicate predictor name")) diff --git a/operator/controllers/ambassador.go b/operator/controllers/ambassador.go index 410794aef6..1affc0e51a 100644 --- a/operator/controllers/ambassador.go +++ b/operator/controllers/ambassador.go @@ -252,10 +252,27 @@ func getAmbassadorConfigs(mlDep *machinelearningv1alpha2.SeldonDeployment, p *ma return "", err } - if GetEnv("AMBASSADOR_SINGLE_NAMESPACE", "false") == "true" { - return YAML_SEP + cRestGlobal + YAML_SEP + cGrpcGlobal + YAML_SEP + cRestNamespaced + YAML_SEP + cGrpcNamespaced, nil + // Return the appropriate set of config based on whether http and/or grpc is active + if engine_http_port > 0 && engine_grpc_port > 0 { + if GetEnv("AMBASSADOR_SINGLE_NAMESPACE", "false") == "true" { + return YAML_SEP + cRestGlobal + YAML_SEP + cGrpcGlobal + YAML_SEP + cRestNamespaced + YAML_SEP + cGrpcNamespaced, nil + } else { + return YAML_SEP + cRestGlobal + YAML_SEP + cGrpcGlobal, nil + } + } else if engine_http_port > 0 { + if GetEnv("AMBASSADOR_SINGLE_NAMESPACE", "false") == "true" { + return YAML_SEP + cRestGlobal + YAML_SEP + cRestNamespaced, nil + } else { + return YAML_SEP + cRestGlobal, nil + } + } else if engine_grpc_port > 0 { + if GetEnv("AMBASSADOR_SINGLE_NAMESPACE", "false") == "true" { + return YAML_SEP + cGrpcGlobal + YAML_SEP + cGrpcNamespaced, nil + } else { + return YAML_SEP + cGrpcGlobal, nil + } } else { - return YAML_SEP + cRestGlobal + YAML_SEP + cGrpcGlobal, nil + return "", nil } } diff --git a/operator/controllers/noengine_test.go b/operator/controllers/noengine_test.go new file mode 100644 index 0000000000..9f503a80b0 --- /dev/null +++ b/operator/controllers/noengine_test.go @@ -0,0 +1,87 @@ +package controllers + +import ( + "context" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "time" +) + +var _ = Describe("Create a Seldon Deployment without engine", func() { + const timeout = time.Second * 30 + const interval = time.Second * 1 + By("Creating a resource") + It("should create a resource with defaults", func() { + Expect(k8sClient).NotTo(BeNil()) + var modelType = machinelearningv1alpha2.MODEL + key := types.NamespacedName{ + Name: "dep2", + Namespace: "default", + } + instance := &machinelearningv1alpha2.SeldonDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: machinelearningv1alpha2.SeldonDeploymentSpec{ + Name: "mydep2", + Predictors: []machinelearningv1alpha2.PredictorSpec{ + { + Annotations: map[string]string{ + "seldon.io/no-engine": "true", + }, + Name: "p1", + ComponentSpecs: []*machinelearningv1alpha2.SeldonPodSpec{ + { + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Image: "seldonio/mock_classifier:1.0", + Name: "classifier", + }, + }, + }, + }, + }, + Graph: &machinelearningv1alpha2.PredictiveUnit{ + Name: "classifier", + Type: &modelType, + }, + }, + }, + }, + } + + // Run Defaulter + instance.Default() + + Expect(k8sClient.Create(context.Background(), instance)).Should(Succeed()) + //time.Sleep(time.Second * 5) + + fetched := &machinelearningv1alpha2.SeldonDeployment{} + Eventually(func() error { + err := k8sClient.Get(context.Background(), key, fetched) + return err + }, timeout, interval).Should(BeNil()) + Expect(fetched.Spec.Name).Should(Equal("mydep2")) + + depKey := types.NamespacedName{ + Name: machinelearningv1alpha2.GetDeploymentName(instance, instance.Spec.Predictors[0], instance.Spec.Predictors[0].ComponentSpecs[0]), + Namespace: "default", + } + depFetched := &appsv1.Deployment{} + Eventually(func() error { + err := k8sClient.Get(context.Background(), depKey, depFetched) + return err + }, timeout, interval).Should(BeNil()) + Expect(len(depFetched.Spec.Template.Spec.Containers)).Should(Equal(1)) + + Expect(k8sClient.Delete(context.Background(), instance)).Should(Succeed()) + }) + +}) diff --git a/operator/controllers/seldondeployment_controller.go b/operator/controllers/seldondeployment_controller.go index ebda826335..37af570649 100644 --- a/operator/controllers/seldondeployment_controller.go +++ b/operator/controllers/seldondeployment_controller.go @@ -84,6 +84,11 @@ type serviceDetails struct { ambassadorUrl string } +type httpGrpcPorts struct { + httpPort int + grpcPort int +} + func createHpa(podSpec *machinelearningv1alpha2.SeldonPodSpec, deploymentName string, seldonId string, namespace string) *autoscaling.HorizontalPodAutoscaler { hpa := autoscaling.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ @@ -113,8 +118,9 @@ func createHpa(podSpec *machinelearningv1alpha2.SeldonPodSpec, deploymentName st func createIstioResources(mlDep *machinelearningv1alpha2.SeldonDeployment, seldonId string, namespace string, - engine_http_port int, - engine_grpc_port int) ([]*istio.VirtualService, []*istio.DestinationRule) { + ports []httpGrpcPorts, + httpAllowed bool, + grpcAllowed bool) ([]*istio.VirtualService, []*istio.DestinationRule) { istio_gateway := GetEnv(ENV_ISTIO_GATEWAY, "seldon-gateway") httpVsvc := &istio.VirtualService{ @@ -197,7 +203,7 @@ func createIstioResources(mlDep *machinelearningv1alpha2.SeldonDeployment, Host: pSvcName, Subset: p.Name, Port: istio.PortSelector{ - Number: uint32(engine_http_port), + Number: uint32(ports[i].httpPort), }, }, Weight: int(p.Traffic), @@ -207,7 +213,7 @@ func createIstioResources(mlDep *machinelearningv1alpha2.SeldonDeployment, Host: pSvcName, Subset: p.Name, Port: istio.PortSelector{ - Number: uint32(engine_grpc_port), + Number: uint32(ports[i].grpcPort), }, }, Weight: int(p.Traffic), @@ -216,11 +222,20 @@ func createIstioResources(mlDep *machinelearningv1alpha2.SeldonDeployment, } httpVsvc.Spec.HTTP[0].Route = routesHttp grpcVsvc.Spec.HTTP[0].Route = routesGrpc - vscs := make([]*istio.VirtualService, 2) - vscs[0] = httpVsvc - vscs[1] = grpcVsvc - - return vscs, drules + if httpAllowed && grpcAllowed { + vscs := make([]*istio.VirtualService, 2) + vscs[0] = httpVsvc + vscs[1] = grpcVsvc + return vscs, drules + } else if httpAllowed { + vscs := make([]*istio.VirtualService, 1) + vscs[0] = httpVsvc + return vscs, drules + } else { + vscs := make([]*istio.VirtualService, 1) + vscs[0] = grpcVsvc + return vscs, drules + } } func getEngineHttpPort() (engine_http_port int, err error) { @@ -266,13 +281,37 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp return nil, err } + // variables to collect what ports will be exposed and whether we should expose http and grpc externally + // If one of the predictors has noEngine then only one of http or grpc should be allowed dependent on + // the type of the noEngine model: whether it is http or grpc + externalPorts := make([]httpGrpcPorts, len(mlDep.Spec.Predictors)) + grpcAllowed := true + httpAllowed := true + // Attempt to set httpAllowed and grpcAllowed to false if we have an noEngine predictor for i := 0; i < len(mlDep.Spec.Predictors); i++ { p := mlDep.Spec.Predictors[i] + _, noEngine := p.Annotations[machinelearningv1alpha2.ANNOTATION_NO_ENGINE] + if noEngine && len(p.ComponentSpecs) > 0 && len(p.ComponentSpecs[0].Spec.Containers) > 0 { + pu := machinelearningv1alpha2.GetPredictiveUnit(p.Graph, p.ComponentSpecs[0].Spec.Containers[0].Name) + if pu != nil { + if pu.Endpoint != nil && pu.Endpoint.Type == machinelearningv1alpha2.GRPC { + httpAllowed = false + } + if pu.Endpoint == nil || pu.Endpoint.Type == machinelearningv1alpha2.REST { + grpcAllowed = false + } + } + } + } + + for i := 0; i < len(mlDep.Spec.Predictors); i++ { + p := mlDep.Spec.Predictors[i] + _, noEngine := p.Annotations[machinelearningv1alpha2.ANNOTATION_NO_ENGINE] pSvcName := machinelearningv1alpha2.GetPredictorKey(mlDep, &p) log.Info("pSvcName", "val", pSvcName) // Add engine deployment if separate _, hasSeparateEnginePod := mlDep.Spec.Annotations[machinelearningv1alpha2.ANNOTATION_SEPARATE_ENGINE] - if hasSeparateEnginePod { + if hasSeparateEnginePod && !noEngine { deploy, err := createEngineDeployment(mlDep, &p, pSvcName, engine_http_port, engine_grpc_port) if err != nil { return nil, err @@ -318,6 +357,43 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp // a user-supplied container may not be a pu so we may not create service for that log.Info("Not creating container service for " + con.Name) } + + if noEngine { + deploy.ObjectMeta.Labels[machinelearningv1alpha2.Label_seldon_app] = pSvcName + deploy.Spec.Selector.MatchLabels[machinelearningv1alpha2.Label_seldon_app] = pSvcName + deploy.Spec.Template.ObjectMeta.Labels[machinelearningv1alpha2.Label_seldon_app] = pSvcName + + port := int(svc.Spec.Ports[0].Port) + if svc.Spec.Ports[0].Name == "grpc" { + httpAllowed = false + externalPorts[i] = httpGrpcPorts{httpPort: 0, grpcPort: port} + psvc, err := createPredictorService(pSvcName, seldonId, &p, mlDep, 0, port, "", log) + if err != nil { + return nil, err + } + + c.services = append(c.services, psvc) + + c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + SvcName: pSvcName, + GrpcEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(port), + } + } else { + externalPorts[i] = httpGrpcPorts{httpPort: port, grpcPort: 0} + grpcAllowed = false + psvc, err := createPredictorService(pSvcName, seldonId, &p, mlDep, port, 0, "", log) + if err != nil { + return nil, err + } + + c.services = append(c.services, psvc) + + c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + SvcName: pSvcName, + HttpEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(port), + } + } + } } } c.deployments = append(c.deployments, deploy) @@ -328,51 +404,76 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp return nil, err } - // Add service orchestrator to engine deployment if needed - if !hasSeparateEnginePod { - var deploy *appsv1.Deployment - found := false + if !noEngine { - // find the pu that the webhook marked as localhost as its corresponding deployment should get the engine - pu := machinelearningv1alpha2.GetEnginePredictiveUnit(p.Graph) - if pu == nil { - // below should never happen - if it did would suggest problem in webhook - return nil, fmt.Errorf("Engine not separate and no pu with localhost service - not clear where to inject engine") - } - // find the deployment with a container for the pu marked for engine - for i, _ := range c.deployments { - dep := c.deployments[i] - for _, con := range dep.Spec.Template.Spec.Containers { - if strings.Compare(con.Name, pu.Name) == 0 { - deploy = dep - found = true + // Add service orchestrator to engine deployment if needed + if !hasSeparateEnginePod { + var deploy *appsv1.Deployment + found := false + + // find the pu that the webhook marked as localhost as its corresponding deployment should get the engine + pu := machinelearningv1alpha2.GetEnginePredictiveUnit(p.Graph) + if pu == nil { + // below should never happen - if it did would suggest problem in webhook + return nil, fmt.Errorf("Engine not separate and no pu with localhost service - not clear where to inject engine") + } + // find the deployment with a container for the pu marked for engine + for i, _ := range c.deployments { + dep := c.deployments[i] + for _, con := range dep.Spec.Template.Spec.Containers { + if strings.Compare(con.Name, pu.Name) == 0 { + deploy = dep + found = true + } } } + + if !found { + // by this point we should have created the Deployment corresponding to the pu marked localhost - if we haven't something has gone wrong + return nil, fmt.Errorf("Engine not separate and no deployment for pu with localhost service - not clear where to inject engine") + } + err := addEngineToDeployment(mlDep, &p, engine_http_port, engine_grpc_port, pSvcName, deploy) + if err != nil { + return nil, err + } + } - if !found { - // by this point we should have created the Deployment corresponding to the pu marked localhost - if we haven't something has gone wrong - return nil, fmt.Errorf("Engine not separate and no deployment for pu with localhost service - not clear where to inject engine") + //Create Service for Predictor - exposed externally (ambassador or istio) and points at engine + httpPort := engine_http_port + if httpAllowed == false { + httpPort = 0 + } + grpcPort := engine_grpc_port + if grpcAllowed == false { + grpcPort = 0 } - err := addEngineToDeployment(mlDep, &p, engine_http_port, engine_grpc_port, pSvcName, deploy) + psvc, err := createPredictorService(pSvcName, seldonId, &p, mlDep, httpPort, grpcPort, "", log) if err != nil { + return nil, err } - } - - //Create Service for Predictor - exposed externally (ambassador or istio) and points at engine - psvc, err := createPredictorService(pSvcName, seldonId, &p, mlDep, engine_http_port, engine_grpc_port, "", log) - if err != nil { - - return nil, err - } + c.services = append(c.services, psvc) + if httpAllowed && grpcAllowed { + c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + SvcName: pSvcName, + HttpEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_http_port), + GrpcEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_grpc_port), + } + } else if httpAllowed { + c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + SvcName: pSvcName, + HttpEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_http_port), + } + } else if grpcAllowed { + c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + SvcName: pSvcName, + GrpcEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_grpc_port), + } + } - c.services = append(c.services, psvc) - c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ - SvcName: pSvcName, - HttpEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_http_port), - GrpcEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_grpc_port), + externalPorts[i] = httpGrpcPorts{httpPort: httpPort, grpcPort: grpcPort} } err = createExplainer(r, mlDep, &p, &c, pSvcName, log) @@ -383,7 +484,7 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp //TODO Fixme - not changed to handle per predictor scenario if GetEnv(ENV_ISTIO_ENABLED, "false") == "true" { - vsvcs, dstRule := createIstioResources(mlDep, seldonId, namespace, engine_http_port, engine_grpc_port) + vsvcs, dstRule := createIstioResources(mlDep, seldonId, namespace, externalPorts, httpAllowed, grpcAllowed) c.virtualServices = append(c.virtualServices, vsvcs...) c.destinationRules = append(c.destinationRules, dstRule...) } @@ -458,13 +559,13 @@ func createContainerService(deploy *appsv1.Deployment, p machinelearningv1alpha2 portNum = existingPort.ContainerPort } - if pu != nil && pu.Endpoint.Type == machinelearningv1alpha2.GRPC { + if pu.Endpoint.Type == machinelearningv1alpha2.GRPC { portType = "grpc" } // pu should have a port set by seldondeployment_create_update_handler.go (if not by user) // that mutator modifies SeldonDeployment and fires before this controller - if pu != nil && pu.Endpoint.ServicePort != 0 { + if pu.Endpoint.ServicePort != 0 { portNum = pu.Endpoint.ServicePort } diff --git a/operator/controllers/seldondeployment_controller_test.go b/operator/controllers/seldondeployment_controller_test.go index 96f11b6397..6d06720fd5 100644 --- a/operator/controllers/seldondeployment_controller_test.go +++ b/operator/controllers/seldondeployment_controller_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/gomega" machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" "io/ioutil" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -29,15 +30,13 @@ import ( "time" ) -const timeout = time.Second * 5 - func helperLoadBytes(name string) []byte { path := filepath.Join("testdata", name) // relative path bytes, _ := ioutil.ReadFile(path) return bytes } -var _ = Describe("Create a deployment", func() { +var _ = Describe("Create a Seldon Deployment", func() { const timeout = time.Second * 30 const interval = time.Second * 1 By("Creating a resource") @@ -78,61 +77,33 @@ var _ = Describe("Create a deployment", func() { }, }, } + + // Run Defaulter + instance.Default() + Expect(k8sClient.Create(context.Background(), instance)).Should(Succeed()) - time.Sleep(time.Second * 5) + //time.Sleep(time.Second * 5) fetched := &machinelearningv1alpha2.SeldonDeployment{} Eventually(func() error { err := k8sClient.Get(context.Background(), key, fetched) return err }, timeout, interval).Should(BeNil()) - Expect(fetched.Spec.Name).Should(Equal("mydep")) - }) - -}) - -func coreDeploymentTests(instance *machinelearningv1alpha2.SeldonDeployment) { - - // Create the SeldonDeployment object and expect the Reconcile and Deployment to be created - Expect(k8sClient.Create(context.Background(), instance)).Should(Succeed()) - time.Sleep(time.Second * 5) - err := k8sClient.Create(context.TODO(), instance) - - Expect(err).NotTo(HaveOccurred()) - // delete the SeldonDeployment at end of test - defer k8sClient.Delete(context.TODO(), instance) - -} - -/* -func testReconcileSimpleModel(t *testing.T) { - instance := &machinelearningv1alpha2.SeldonDeployment{} - - bStr := helperLoadBytes("model.json") - json.Unmarshal(bStr, instance) - - coreDeploymentTests(instance) -} - -func testReconcileModelLongName(t *testing.T) { - instance := &machinelearningv1alpha2.SeldonDeployment{} - - bStr := helperLoadBytes("model_long_name.json") - json.Unmarshal(bStr, instance) - - coreDeploymentTests(instance) -} - -func testReconcileHpaModel(t *testing.T) { - - instance := &machinelearningv1alpha2.SeldonDeployment{} + depKey := types.NamespacedName{ + Name: machinelearningv1alpha2.GetDeploymentName(instance, instance.Spec.Predictors[0], instance.Spec.Predictors[0].ComponentSpecs[0]), + Namespace: "default", + } + depFetched := &appsv1.Deployment{} + Eventually(func() error { + err := k8sClient.Get(context.Background(), depKey, depFetched) + return err + }, timeout, interval).Should(BeNil()) + Expect(len(depFetched.Spec.Template.Spec.Containers)).Should(Equal(2)) - bStr := helperLoadBytes("model_with_hpa.json") - json.Unmarshal(bStr, instance) + Expect(k8sClient.Delete(context.Background(), instance)).Should(Succeed()) - coreDeploymentTests(instance) -} + }) -*/ +}) diff --git a/operator/controllers/seldondeployment_prepackaged_servers_test.go b/operator/controllers/seldondeployment_prepackaged_servers_test.go new file mode 100644 index 0000000000..b2901e97cb --- /dev/null +++ b/operator/controllers/seldondeployment_prepackaged_servers_test.go @@ -0,0 +1,77 @@ +package controllers + +import ( + "context" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + "github.com/seldonio/seldon-core/operator/utils" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "time" +) + +var _ = Describe("Create a prepacked sklearn server", func() { + const timeout = time.Second * 30 + const interval = time.Second * 1 + By("Creating a resource") + It("should create a resource with defaults", func() { + Expect(k8sClient).NotTo(BeNil()) + var modelType = machinelearningv1alpha2.MODEL + var impl = machinelearningv1alpha2.SKLEARN_SERVER + key := types.NamespacedName{ + Name: "prepack", + Namespace: "default", + } + instance := &machinelearningv1alpha2.SeldonDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: machinelearningv1alpha2.SeldonDeploymentSpec{ + Name: "pp", + Predictors: []machinelearningv1alpha2.PredictorSpec{ + { + Name: "p1", + Graph: &machinelearningv1alpha2.PredictiveUnit{ + Name: "classifier", + Type: &modelType, + Implementation: &impl, + Endpoint: &machinelearningv1alpha2.Endpoint{Type: machinelearningv1alpha2.REST}, + }, + }, + }, + }, + } + + // Run Defaulter + instance.Default() + + Expect(k8sClient.Create(context.Background(), instance)).Should(Succeed()) + //time.Sleep(time.Second * 5) + + fetched := &machinelearningv1alpha2.SeldonDeployment{} + Eventually(func() error { + err := k8sClient.Get(context.Background(), key, fetched) + return err + }, timeout, interval).Should(BeNil()) + Expect(fetched.Spec.Name).Should(Equal("pp")) + + sPodSpec := utils.GetSeldonPodSpecForPredictiveUnit(&instance.Spec.Predictors[0], instance.Spec.Predictors[0].Graph.Name) + depName := machinelearningv1alpha2.GetDeploymentName(instance, instance.Spec.Predictors[0], sPodSpec) + depKey := types.NamespacedName{ + Name: depName, + Namespace: "default", + } + depFetched := &appsv1.Deployment{} + Eventually(func() error { + err := k8sClient.Get(context.Background(), depKey, depFetched) + return err + }, timeout, interval).Should(BeNil()) + Expect(len(depFetched.Spec.Template.Spec.Containers)).Should(Equal(2)) + + Expect(k8sClient.Delete(context.Background(), instance)).Should(Succeed()) + }) + +}) diff --git a/operator/controllers/suite_test.go b/operator/controllers/suite_test.go index 80aac25576..3f97997644 100644 --- a/operator/controllers/suite_test.go +++ b/operator/controllers/suite_test.go @@ -17,6 +17,9 @@ limitations under the License. package controllers import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "os" "path/filepath" "testing" @@ -24,8 +27,13 @@ import ( . "github.com/onsi/gomega" machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" - "k8s.io/client-go/kubernetes/scheme" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/rest" + istio "knative.dev/pkg/apis/istio/v1alpha3" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -38,7 +46,10 @@ import ( var cfg *rest.Config var k8sClient client.Client +var k8sManager ctrl.Manager var testEnv *envtest.Environment +var scheme = runtime.NewScheme() +var clientset *kubernetes.Clientset func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -57,7 +68,8 @@ var _ = BeforeSuite(func(done Done) { //apiServerFlags = append(apiServerFlags, "--admission-control=MutatingAdmissionWebhook") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases"), + filepath.Join("..", "testing")}, } var err error @@ -65,13 +77,55 @@ var _ = BeforeSuite(func(done Done) { Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) - err = machinelearningv1alpha2.AddToScheme(scheme.Scheme) + clientset, err = kubernetes.NewForConfig(cfg) + Expect(err).NotTo(HaveOccurred()) + + err = os.Setenv(ENV_ISTIO_ENABLED, "true") + Expect(err).NotTo(HaveOccurred()) + + err = clientgoscheme.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //err = scheme.AddToScheme(scheme.Scheme) + //Expect(err).NotTo(HaveOccurred()) + + err = appsv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = corev1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = machinelearningv1alpha2.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = istio.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + LeaderElection: false, + }) Expect(err).ToNot(HaveOccurred()) + + err = (&SeldonDeploymentReconciler{ + Client: k8sManager.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("SeldonDeployment"), + Scheme: k8sManager.GetScheme(), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + //err = (&machinelearningv1alpha2.SeldonDeployment{}).SetupWebhookWithManager(k8sManager) + //Expect(err).ToNot(HaveOccurred()) + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + //k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + k8sClient = k8sManager.GetClient() Expect(k8sClient).ToNot(BeNil()) close(done) diff --git a/operator/go.mod b/operator/go.mod index 398adfa1d4..51f131df2e 100644 --- a/operator/go.mod +++ b/operator/go.mod @@ -3,12 +3,15 @@ module github.com/seldonio/seldon-core/operator go 1.12 require ( + github.com/Azure/go-autorest/autorest v0.9.2 // indirect + github.com/Azure/go-autorest/autorest/adal v0.7.0 // indirect github.com/go-logr/logr v0.1.0 github.com/gogo/protobuf v1.2.1 // indirect github.com/golang/protobuf v1.3.1 // indirect github.com/google/go-cmp v0.3.1 github.com/google/gofuzz v1.0.0 // indirect github.com/googleapis/gnostic v0.3.0 // indirect + github.com/gophercloud/gophercloud v0.4.0 // indirect github.com/json-iterator/go v1.1.6 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/onsi/ginkgo v1.8.0 diff --git a/operator/go.sum b/operator/go.sum index 32d30850e7..eaec5b66c9 100644 --- a/operator/go.sum +++ b/operator/go.sum @@ -1,5 +1,21 @@ cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.2 h1:6AWuh3uWrsZJcNoCHrCF/+g4aKPCU39kaMO6/qrnK/4= +github.com/Azure/go-autorest/autorest v0.9.2/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.7.0 h1:PUMxSVw3tEImG0JTRqbxjXLKCSoPk7DartDELqlOuiI= +github.com/Azure/go-autorest/autorest/adal v0.7.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= @@ -9,6 +25,8 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -46,6 +64,8 @@ github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhp github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0= github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.4.0 h1:4iXQnHF7LKOl7ncQsRibnUmfx/unxT3rLAniYRB8kQQ= +github.com/gophercloud/gophercloud v0.4.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 h1:UnszMmmmm5vLwWzDjTFVIkfhvWF1NdrmChl8L2NUDCw= github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -118,6 +138,7 @@ go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= @@ -136,6 +157,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190621203818-d432491b9138 h1:t8BZD9RDjkm9/h7yYN6kE8oaeov5r9aztkB7zKA5Tkg= diff --git a/operator/testing/istio-crd-10.yaml b/operator/testing/istio-crd-10.yaml new file mode 100644 index 0000000000..e76e50edc5 --- /dev/null +++ b/operator/testing/istio-crd-10.yaml @@ -0,0 +1,636 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: virtualservices.networking.istio.io + labels: + app: istio-pilot + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: networking.istio.io + names: + kind: VirtualService + listKind: VirtualServiceList + plural: virtualservices + singular: virtualservice + shortNames: + - vs + categories: + - istio-io + - networking-istio-io + scope: Namespaced + versions: + - name: v1alpha3 + served: true + storage: true + additionalPrinterColumns: + - JSONPath: .spec.gateways + description: The names of gateways and sidecars that should apply these routes + name: Gateways + type: string + - JSONPath: .spec.hosts + description: The destination hosts to which traffic is being sent + name: Hosts + type: string + - JSONPath: .metadata.creationTimestamp + description: |- + CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + name: Age + type: date +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: destinationrules.networking.istio.io + labels: + app: istio-pilot + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: networking.istio.io + names: + kind: DestinationRule + listKind: DestinationRuleList + plural: destinationrules + singular: destinationrule + shortNames: + - dr + categories: + - istio-io + - networking-istio-io + scope: Namespaced + versions: + - name: v1alpha3 + served: true + storage: true + additionalPrinterColumns: + - JSONPath: .spec.host + description: The name of a service from the service registry + name: Host + type: string + - JSONPath: .metadata.creationTimestamp + description: |- + CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + name: Age + type: date +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: serviceentries.networking.istio.io + labels: + app: istio-pilot + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: networking.istio.io + names: + kind: ServiceEntry + listKind: ServiceEntryList + plural: serviceentries + singular: serviceentry + shortNames: + - se + categories: + - istio-io + - networking-istio-io + scope: Namespaced + versions: + - name: v1alpha3 + served: true + storage: true + additionalPrinterColumns: + - JSONPath: .spec.hosts + description: The hosts associated with the ServiceEntry + name: Hosts + type: string + - JSONPath: .spec.location + description: Whether the service is external to the mesh or part of the mesh (MESH_EXTERNAL or MESH_INTERNAL) + name: Location + type: string + - JSONPath: .spec.resolution + description: Service discovery mode for the hosts (NONE, STATIC, or DNS) + name: Resolution + type: string + - JSONPath: .metadata.creationTimestamp + description: |- + CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + name: Age + type: date +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: gateways.networking.istio.io + labels: + app: istio-pilot + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: networking.istio.io + names: + kind: Gateway + plural: gateways + singular: gateway + shortNames: + - gw + categories: + - istio-io + - networking-istio-io + scope: Namespaced + versions: + - name: v1alpha3 + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: envoyfilters.networking.istio.io + labels: + app: istio-pilot + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: networking.istio.io + names: + kind: EnvoyFilter + plural: envoyfilters + singular: envoyfilter + categories: + - istio-io + - networking-istio-io + scope: Namespaced + versions: + - name: v1alpha3 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: clusterrbacconfigs.rbac.istio.io + labels: + app: istio-pilot + istio: rbac + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: rbac.istio.io + names: + kind: ClusterRbacConfig + plural: clusterrbacconfigs + singular: clusterrbacconfig + categories: + - istio-io + - rbac-istio-io + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: policies.authentication.istio.io + labels: + app: istio-citadel + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: authentication.istio.io + names: + kind: Policy + plural: policies + singular: policy + categories: + - istio-io + - authentication-istio-io + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: meshpolicies.authentication.istio.io + labels: + app: istio-citadel + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: authentication.istio.io + names: + kind: MeshPolicy + listKind: MeshPolicyList + plural: meshpolicies + singular: meshpolicy + categories: + - istio-io + - authentication-istio-io + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: httpapispecbindings.config.istio.io + labels: + app: istio-mixer + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: HTTPAPISpecBinding + plural: httpapispecbindings + singular: httpapispecbinding + categories: + - istio-io + - apim-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: httpapispecs.config.istio.io + labels: + app: istio-mixer + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: HTTPAPISpec + plural: httpapispecs + singular: httpapispec + categories: + - istio-io + - apim-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: quotaspecbindings.config.istio.io + labels: + app: istio-mixer + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: QuotaSpecBinding + plural: quotaspecbindings + singular: quotaspecbinding + categories: + - istio-io + - apim-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: quotaspecs.config.istio.io + labels: + app: istio-mixer + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: QuotaSpec + plural: quotaspecs + singular: quotaspec + categories: + - istio-io + - apim-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: rules.config.istio.io + labels: + app: mixer + package: istio.io.mixer + istio: core + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: rule + plural: rules + singular: rule + categories: + - istio-io + - policy-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: attributemanifests.config.istio.io + labels: + app: mixer + package: istio.io.mixer + istio: core + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: attributemanifest + plural: attributemanifests + singular: attributemanifest + categories: + - istio-io + - policy-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: rbacconfigs.rbac.istio.io + labels: + app: mixer + package: istio.io.mixer + istio: rbac + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: rbac.istio.io + names: + kind: RbacConfig + plural: rbacconfigs + singular: rbacconfig + categories: + - istio-io + - rbac-istio-io + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: serviceroles.rbac.istio.io + labels: + app: mixer + package: istio.io.mixer + istio: rbac + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: rbac.istio.io + names: + kind: ServiceRole + plural: serviceroles + singular: servicerole + categories: + - istio-io + - rbac-istio-io + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: servicerolebindings.rbac.istio.io + labels: + app: mixer + package: istio.io.mixer + istio: rbac + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: rbac.istio.io + names: + kind: ServiceRoleBinding + plural: servicerolebindings + singular: servicerolebinding + categories: + - istio-io + - rbac-istio-io + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - JSONPath: .spec.roleRef.name + description: The name of the ServiceRole object being referenced + name: Reference + type: string + - JSONPath: .metadata.creationTimestamp + description: |- + CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + name: Age + type: date +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: adapters.config.istio.io + labels: + app: mixer + package: adapter + istio: mixer-adapter + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: adapter + plural: adapters + singular: adapter + categories: + - istio-io + - policy-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: instances.config.istio.io + labels: + app: mixer + package: instance + istio: mixer-instance + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: instance + plural: instances + singular: instance + categories: + - istio-io + - policy-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: templates.config.istio.io + labels: + app: mixer + package: template + istio: mixer-template + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: template + plural: templates + singular: template + categories: + - istio-io + - policy-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- +kind: CustomResourceDefinition +apiVersion: apiextensions.k8s.io/v1beta1 +metadata: + name: handlers.config.istio.io + labels: + app: mixer + package: handler + istio: mixer-handler + chart: istio + heritage: Tiller + release: istio + annotations: + "helm.sh/resource-policy": keep +spec: + group: config.istio.io + names: + kind: handler + plural: handlers + singular: handler + categories: + - istio-io + - policy-istio-io + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true +--- diff --git a/python/seldon_core/wrapper.py b/python/seldon_core/wrapper.py index f69422b19f..28c141804b 100644 --- a/python/seldon_core/wrapper.py +++ b/python/seldon_core/wrapper.py @@ -35,6 +35,7 @@ def openAPI(): return send_from_directory('', "openapi/seldon.json") @app.route("/predict", methods=["GET", "POST"]) + @app.route("/api/v0.1/predictions", methods=["POST"]) def Predict(): requestJson = get_request() logger.debug("REST Request: %s", request) @@ -43,6 +44,7 @@ def Predict(): return jsonify(response) @app.route("/send-feedback", methods=["GET", "POST"]) + @app.route("/api/v0.1/feedback", methods=["POST"]) def SendFeedback(): requestJson = get_request() logger.debug("REST Request: %s", request) @@ -139,5 +141,6 @@ def get_grpc_server(user_model, annotations={}, trace_interceptor=None): prediction_pb2_grpc.add_GenericServicer_to_server(seldon_model, server) prediction_pb2_grpc.add_ModelServicer_to_server(seldon_model, server) prediction_pb2_grpc.add_TransformerServicer_to_server(seldon_model, server) + prediction_pb2_grpc.add_SeldonServicer_to_server(seldon_model, server) return server diff --git a/python/setup.py b/python/setup.py index c26a22aefc..4dde0e3cd7 100644 --- a/python/setup.py +++ b/python/setup.py @@ -38,7 +38,7 @@ "minio >= 4.0.9", "google-cloud-storage >= 1.16.0", "azure-storage-blob >= 2.0.1", - "setuptools>=41.0.0" + "setuptools >= 41.0.0" ], tests_require=[ 'pytest',