From 64d1978d0b3eef59344c22f0f6d341b8e42da90f Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Tue, 10 Dec 2019 14:18:22 +0000 Subject: [PATCH 01/12] Add v1 of the CRD --- operator/api/v1/groupversion_info.go | 36 ++ operator/api/v1/seldondeployment_types.go | 391 ++++++++++++++ operator/api/v1/seldondeployment_webhook.go | 537 ++++++++++++++++++++ operator/api/v1/zz_generated.deepcopy.go | 443 ++++++++++++++++ 4 files changed, 1407 insertions(+) create mode 100644 operator/api/v1/groupversion_info.go create mode 100644 operator/api/v1/seldondeployment_types.go create mode 100644 operator/api/v1/seldondeployment_webhook.go create mode 100644 operator/api/v1/zz_generated.deepcopy.go diff --git a/operator/api/v1/groupversion_info.go b/operator/api/v1/groupversion_info.go new file mode 100644 index 0000000000..bc971f123a --- /dev/null +++ b/operator/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +// Package v1 contains API Schema definitions for the machinelearning v1 API group +// +kubebuilder:object:generate=true +// +groupName=machinelearning.seldon.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "machinelearning.seldon.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/operator/api/v1/seldondeployment_types.go b/operator/api/v1/seldondeployment_types.go new file mode 100644 index 0000000000..d679cee2e3 --- /dev/null +++ b/operator/api/v1/seldondeployment_types.go @@ -0,0 +1,391 @@ +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +package v1 + +import ( + "crypto/md5" + "encoding/hex" + "regexp" + "strings" + + autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + Label_seldon_id = "seldon-deployment-id" + Label_seldon_app = "seldon-app" + Label_svc_orch = "seldon-deployment-contains-svcorch" + + PODINFO_VOLUME_NAME = "podinfo" + PODINFO_VOLUME_PATH = "/etc/podinfo" + + ENV_PREDICTIVE_UNIT_SERVICE_PORT = "PREDICTIVE_UNIT_SERVICE_PORT" + ENV_PREDICTIVE_UNIT_PARAMETERS = "PREDICTIVE_UNIT_PARAMETERS" + ENV_PREDICTIVE_UNIT_ID = "PREDICTIVE_UNIT_ID" + ENV_PREDICTOR_ID = "PREDICTOR_ID" + ENV_SELDON_DEPLOYMENT_ID = "SELDON_DEPLOYMENT_ID" + + 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" +) + +func hash(text string) string { + hasher := md5.New() + hasher.Write([]byte(text)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func containerHash(podSpec *SeldonPodSpec) string { + s := []string{} + for i := 0; i < len(podSpec.Spec.Containers); i++ { + c := podSpec.Spec.Containers[i] + s = append(s, c.Name) + s = append(s, c.Image) + } + key := strings.Join(s, ":") + ";" + return hash(key)[:7] +} + +func createPredictorHash(p *PredictorSpec) string { + s := []string{} + for i := 0; i < len(p.ComponentSpecs); i++ { + s = append(s, containerHash(p.ComponentSpecs[i])) + } + key := strings.Join(s, ",") + "," + return hash(key)[:7] +} + +func GetSeldonDeploymentName(mlDep *SeldonDeployment) string { + name := mlDep.Spec.Name + "-" + mlDep.ObjectMeta.Name + if len(name) > 63 { + return "seldon-" + hash(name) + } else { + return name + } +} + +func GetExplainerDeploymentName(sdepName string, predictorSpec *PredictorSpec) string { + name := sdepName + "-" + predictorSpec.Name + "-explainer" + if len(name) > 63 { + return "seldon-" + hash(name) + } else { + return name + } +} + +func GetDeploymentName(mlDep *SeldonDeployment, predictorSpec PredictorSpec, podSpec *SeldonPodSpec) string { + if podSpec != nil && len(podSpec.Metadata.Name) != 0 { + return podSpec.Metadata.Name + } else { + name := mlDep.Spec.Name + "-" + predictorSpec.Name + if podSpec != nil { + name = name + "-" + containerHash(podSpec) + } + if len(name) > 63 { + return "seldon-" + hash(name) + } else { + return name + } + } +} + +func GetServiceOrchestratorName(mlDep *SeldonDeployment, p *PredictorSpec) string { + svcOrchName := mlDep.Spec.Name + "-" + p.Name + "-svc-orch" + "-" + createPredictorHash(p) + if len(svcOrchName) > 63 { + return "seldon-" + hash(svcOrchName) + } else { + return svcOrchName + } +} + +func GetPredictorKey(mlDep *SeldonDeployment, p *PredictorSpec) string { + if annotation, hasAnnotation := p.Annotations[ANNOTATION_CUSTOM_SVC_NAME]; hasAnnotation { + return annotation + } else { + return getPredictorKeyAutoGenerated(mlDep, p) + } +} + +func getPredictorKeyAutoGenerated(mlDep *SeldonDeployment, p *PredictorSpec) string { + pName := mlDep.Name + "-" + mlDep.Spec.Name + "-" + p.Name + if len(pName) > 63 { + return "seldon-" + hash(pName) + } else { + return pName + } +} + +func GetPredictorServiceNameKey(c *v1.Container) string { + return Label_seldon_app + "-" + c.Name +} + +func GetPredictiveUnit(pu *PredictiveUnit, name string) *PredictiveUnit { + if name == pu.Name { + return pu + } else { + for i := 0; i < len(pu.Children); i++ { + found := GetPredictiveUnit(&pu.Children[i], name) + if found != nil { + return found + } + } + return nil + } +} + +// if engine is not separated then this tells us which pu it should go on, as the mutating webhook handler has set host as localhost on the pu +func GetEnginePredictiveUnit(pu *PredictiveUnit) *PredictiveUnit { + if pu.Endpoint != nil && pu.Endpoint.ServiceHost == "localhost" { + return pu + } else { + for i := 0; i < len(pu.Children); i++ { + found := GetEnginePredictiveUnit(&pu.Children[i]) + if found != nil { + return found + } + } + return nil + } +} + +func GetPredictiveUnitList(p *PredictiveUnit) (list []*PredictiveUnit) { + list = append(list, p) + + for i := 0; i < len(p.Children); i++ { + pu := &p.Children[i] + list = append(list, GetPredictiveUnitList(pu)...) + } + return list +} + +func cleanContainerName(name string) string { + var re = regexp.MustCompile("[^-a-z0-9]") + return re.ReplaceAllString(strings.ToLower(name), "-") +} + +func GetContainerServiceName(mlDep *SeldonDeployment, predictorSpec PredictorSpec, c *v1.Container) string { + containerImageName := cleanContainerName(c.Image) + svcName := mlDep.Spec.Name + "-" + predictorSpec.Name + "-" + c.Name + if containerImageName != "" { + svcName = svcName + "-" + containerImageName + } + if len(svcName) > 63 { + svcName = "seldon" + "-" + containerImageName + "-" + hash(svcName) + if len(svcName) > 63 { + return "seldon-" + hash(svcName) + } else { + return svcName + } + } else { + return svcName + } +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// SeldonDeploymentSpec defines the desired state of SeldonDeployment +type SeldonDeploymentSpec struct { + Name string `json:"name,omitempty" protobuf:"string,1,opt,name=name"` + Predictors []PredictorSpec `json:"predictors" protobuf:"bytes,2,opt,name=name"` + OauthKey string `json:"oauth_key,omitempty" protobuf:"string,3,opt,name=oauth_key"` + OauthSecret string `json:"oauth_secret,omitempty" protobuf:"string,4,opt,name=oauth_secret"` + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,5,opt,name=annotations"` +} + +type PredictorSpec struct { + Name string `json:"name" protobuf:"string,1,opt,name=name"` + Graph *PredictiveUnit `json:"graph" protobuf:"bytes,2,opt,name=predictiveUnit"` + ComponentSpecs []*SeldonPodSpec `json:"componentSpecs,omitempty" protobuf:"bytes,3,opt,name=componentSpecs"` + Replicas int32 `json:"replicas,omitempty" protobuf:"string,4,opt,name=replicas"` + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,5,opt,name=annotations"` + EngineResources v1.ResourceRequirements `json:"engineResources,omitempty" protobuf:"bytes,6,opt,name=engineResources"` + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,7,opt,name=labels"` + SvcOrchSpec SvcOrchSpec `json:"svcOrchSpec,omitempty" protobuf:"bytes,8,opt,name=svcOrchSpec"` + Traffic int32 `json:"traffic,omitempty" protobuf:"bytes,9,opt,name=traffic"` + Explainer Explainer `json:"explainer,omitempty" protobuf:"bytes,10,opt,name=explainer"` + Shadow bool `json:"shadow,omitempty" protobuf:"bytes,11,opt,name=shadow"` +} + +type SvcOrchSpec struct { + Resources *v1.ResourceRequirements `json:"resources,omitempty" protobuf:"bytes,1,opt,name=resources"` + Env []*v1.EnvVar `json:"env,omitempty" protobuf:"bytes,2,opt,name=env"` +} + +type AlibiExplainerType string + +const ( + AlibiAnchorsTabularExplainer AlibiExplainerType = "AnchorTabular" + AlibiAnchorsImageExplainer AlibiExplainerType = "AnchorImages" + AlibiAnchorsTextExplainer AlibiExplainerType = "AnchorText" + AlibiCounterfactualsExplainer AlibiExplainerType = "Counterfactuals" + AlibiContrastiveExplainer AlibiExplainerType = "Contrastive" +) + +type Explainer struct { + Type AlibiExplainerType `json:"type,omitempty" protobuf:"string,1,opt,name=type"` + ModelUri string `json:"modelUri,omitempty" protobuf:"string,2,opt,name=modelUri"` + ServiceAccountName string `json:"serviceAccountName,omitempty" protobuf:"string,3,opt,name=serviceAccountName"` + ContainerSpec v1.Container `json:"containerSpec,omitempty" protobuf:"bytes,4,opt,name=containerSpec"` + Config map[string]string `json:"config,omitempty" protobuf:"bytes,5,opt,name=config"` + Endpoint *Endpoint `json:"endpoint,omitempty" protobuf:"bytes,6,opt,name=endpoint"` + EnvSecretRefName string `json:"envSecretRefName,omitempty" protobuf:"bytes,7,opt,name=envSecretRefName"` +} + +type SeldonPodSpec struct { + Metadata metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + Spec v1.PodSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + HpaSpec *SeldonHpaSpec `json:"hpaSpec,omitempty" protobuf:"bytes,3,opt,name=hpaSpec"` +} + +type SeldonHpaSpec struct { + MinReplicas *int32 `json:"minReplicas,omitempty" protobuf:"int,1,opt,name=minReplicas"` + MaxReplicas int32 `json:"maxReplicas" protobuf:"int,2,opt,name=maxReplicas"` + Metrics []autoscalingv2beta2.MetricSpec `json:"metrics,omitempty" protobuf:"bytes,3,opt,name=metrics"` +} + +type PredictiveUnitType string + +const ( + UNKNOWN_TYPE PredictiveUnitType = "UNKNOWN_TYPE" + ROUTER PredictiveUnitType = "ROUTER" + COMBINER PredictiveUnitType = "COMBINER" + MODEL PredictiveUnitType = "MODEL" + TRANSFORMER PredictiveUnitType = "TRANSFORMER" + OUTPUT_TRANSFORMER PredictiveUnitType = "OUTPUT_TRANSFORMER" +) + +type PredictiveUnitImplementation string + +const ( + UNKNOWN_IMPLEMENTATION PredictiveUnitImplementation = "UNKNOWN_IMPLEMENTATION" + SIMPLE_MODEL PredictiveUnitImplementation = "SIMPLE_MODEL" + SIMPLE_ROUTER PredictiveUnitImplementation = "SIMPLE_ROUTER" + RANDOM_ABTEST PredictiveUnitImplementation = "RANDOM_ABTEST" + AVERAGE_COMBINER PredictiveUnitImplementation = "AVERAGE_COMBINER" +) + +type PredictiveUnitMethod string + +const ( + TRANSFORM_INPUT PredictiveUnitMethod = "TRANSFORM_INPUT" + TRANSFORM_OUTPUT PredictiveUnitMethod = "TRANSFORM_OUTPUT" + ROUTE PredictiveUnitMethod = "ROUTE" + AGGREGATE PredictiveUnitMethod = "AGGREGATE" + SEND_FEEDBACK PredictiveUnitMethod = "SEND_FEEDBACK" +) + +type EndpointType string + +const ( + REST EndpointType = "REST" + GRPC EndpointType = "GRPC" +) + +type Endpoint struct { + ServiceHost string `json:"service_host,omitempty" protobuf:"string,1,opt,name=service_host"` + ServicePort int32 `json:"service_port,omitempty" protobuf:"int32,2,opt,name=service_port"` + Type EndpointType `json:"type,omitempty" protobuf:"int,3,opt,name=type"` +} + +type ParmeterType string + +const ( + INT ParmeterType = "INT" + FLOAT ParmeterType = "FLOAT" + DOUBLE ParmeterType = "DOUBLE" + STRING ParmeterType = "STRING" + BOOL ParmeterType = "BOOL" +) + +type Parameter struct { + Name string `json:"name" protobuf:"string,1,opt,name=name"` + Value string `json:"value" protobuf:"string,2,opt,name=value"` + Type ParmeterType `json:"type" protobuf:"int,3,opt,name=type"` +} + +type PredictiveUnit struct { + Name string `json:"name" protobuf:"string,1,opt,name=name"` + Children []PredictiveUnit `json:"children,omitempty" protobuf:"bytes,2,opt,name=children"` + Type *PredictiveUnitType `json:"type,omitempty" protobuf:"int,3,opt,name=type"` + Implementation *PredictiveUnitImplementation `json:"implementation,omitempty" protobuf:"int,4,opt,name=implementation"` + Methods *[]PredictiveUnitMethod `json:"methods,omitempty" protobuf:"int,5,opt,name=methods"` + Endpoint *Endpoint `json:"endpoint,omitempty" protobuf:"bytes,6,opt,name=endpoint"` + Parameters []Parameter `json:"parameters,omitempty" protobuf:"bytes,7,opt,name=parameters"` + ModelURI string `json:"modelUri,omitempty" protobuf:"bytes,8,opt,name=modelUri"` + ServiceAccountName string `json:"serviceAccountName,omitempty" protobuf:"bytes,9,opt,name=serviceAccountName"` + EnvSecretRefName string `json:"envSecretRefName,omitempty" protobuf:"bytes,10,opt,name=envSecretRefName"` +} + +type DeploymentStatus struct { + Name string `json:"name,omitempty" protobuf:"string,1,opt,name=name"` + Status string `json:"status,omitempty" protobuf:"string,2,opt,name=status"` + Description string `json:"description,omitempty" protobuf:"string,3,opt,name=description"` + Replicas int32 `json:"replicas,omitempty" protobuf:"string,4,opt,name=replicas"` + AvailableReplicas int32 `json:"availableReplicas,omitempty" protobuf:"string,5,opt,name=availableRelicas"` + ExplainerFor string `json:"explainerFor,omitempty" protobuf:"string,6,opt,name=explainerFor"` +} + +type ServiceStatus struct { + SvcName string `json:"svcName,omitempty" protobuf:"string,1,opt,name=svcName"` + HttpEndpoint string `json:"httpEndpoint,omitempty" protobuf:"string,2,opt,name=httpEndpoint"` + GrpcEndpoint string `json:"grpcEndpoint,omitempty" protobuf:"string,3,opt,name=grpcEndpoint"` + ExplainerFor string `json:"explainerFor,omitempty" protobuf:"string,4,opt,name=explainerFor"` +} + +// SeldonDeploymentStatus defines the observed state of SeldonDeployment +type SeldonDeploymentStatus struct { + State string `json:"state,omitempty" protobuf:"string,1,opt,name=state"` + Description string `json:"description,omitempty" protobuf:"string,2,opt,name=description"` + DeploymentStatus map[string]DeploymentStatus `json:"deploymentStatus,omitempty" protobuf:"bytes,3,opt,name=deploymentStatus"` + ServiceStatus map[string]ServiceStatus `json:"serviceStatus,omitempty" protobuf:"bytes,4,opt,name=serviceStatus"` +} + +// +genclient +// +genclient:noStatus +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SeldonDeployment is the Schema for the seldondeployments API +// +k8s:openapi-gen=true +// +kubebuilder:resource:shortName=sdep +// +kubebuilder:subresource:status +type SeldonDeployment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SeldonDeploymentSpec `json:"spec,omitempty"` + Status SeldonDeploymentStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SeldonDeploymentList contains a list of SeldonDeployment +type SeldonDeploymentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SeldonDeployment `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SeldonDeployment{}, &SeldonDeploymentList{}) +} diff --git a/operator/api/v1/seldondeployment_webhook.go b/operator/api/v1/seldondeployment_webhook.go new file mode 100644 index 0000000000..d24a3a6b04 --- /dev/null +++ b/operator/api/v1/seldondeployment_webhook.go @@ -0,0 +1,537 @@ +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + k8types "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + "os" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "strconv" +) + +var ( + // log is for logging in this package. + seldondeploymentlog = logf.Log.WithName("seldondeployment") + ControllerNamespace = GetEnv("POD_NAMESPACE", "seldon-system") + ControllerConfigMapName = "seldon-config" + C client.Client +) + +const PredictorServerConfigMapKeyName = "predictor_servers" + +type PredictorImageConfig struct { + ContainerImage string `json:"image"` + DefaultImageVersion string `json:"defaultImageVersion"` +} + +type PredictorServerConfig struct { + Tensorflow bool `json:"tensorflow,omitempty"` + TensorflowImage string `json:"tfImage,omitempty"` + RestConfig PredictorImageConfig `json:"rest,omitempty"` + GrpcConfig PredictorImageConfig `json:"grpc,omitempty"` +} + +// Get an environment variable given by key or return the fallback. +func GetEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func getPredictorServerConfigs() (map[string]PredictorServerConfig, error) { + configMap := &corev1.ConfigMap{} + + err := C.Get(context.TODO(), k8types.NamespacedName{Name: ControllerConfigMapName, Namespace: ControllerNamespace}, configMap) + + if err != nil { + fmt.Println("Failed to find config map " + ControllerConfigMapName) + fmt.Println(err) + return nil, err + } + return getPredictorServerConfigsFromMap(configMap) +} + +func getPredictorServerConfigsFromMap(configMap *corev1.ConfigMap) (map[string]PredictorServerConfig, error) { + predictorServerConfig := make(map[string]PredictorServerConfig) + if predictorConfig, ok := configMap.Data[PredictorServerConfigMapKeyName]; ok { + err := json.Unmarshal([]byte(predictorConfig), &predictorServerConfig) + if err != nil { + panic(fmt.Errorf("Unable to unmarshall %v json string due to %v ", PredictorServerConfigMapKeyName, err)) + } + } + + return predictorServerConfig, nil +} + +func (r *SeldonDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error { + C = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +var _ webhook.Defaulter = &SeldonDeployment{} + +func GetContainerForPredictiveUnit(p *PredictorSpec, name string) *corev1.Container { + for j := 0; j < len(p.ComponentSpecs); j++ { + cSpec := p.ComponentSpecs[j] + for k := 0; k < len(cSpec.Spec.Containers); k++ { + c := &cSpec.Spec.Containers[k] + if c.Name == name { + return c + } + } + } + return nil +} + +func GetPort(name string, ports []corev1.ContainerPort) *corev1.ContainerPort { + for i := 0; i < len(ports); i++ { + if ports[i].Name == name { + return &ports[i] + } + } + return nil +} + +func IsPrepack(pu *PredictiveUnit) bool { + isPrepack := len(*pu.Implementation) > 0 && *pu.Implementation != SIMPLE_MODEL && *pu.Implementation != SIMPLE_ROUTER && *pu.Implementation != RANDOM_ABTEST && *pu.Implementation != AVERAGE_COMBINER && *pu.Implementation != UNKNOWN_IMPLEMENTATION + return isPrepack +} + +func GetPrepackServerConfig(serverName string) PredictorServerConfig { + ServersConfigs, err := getPredictorServerConfigs() + + if err != nil { + seldondeploymentlog.Error(err, "Failed to read prepacked model servers from configmap") + } + ServerConfig, ok := ServersConfigs[serverName] + if !ok { + seldondeploymentlog.Error(nil, "No entry in predictors map for "+serverName) + } + return ServerConfig +} + +func SetImageNameForPrepackContainer(pu *PredictiveUnit, c *corev1.Container) { + //Add missing fields + // Add image + if c.Image == "" { + + ServerConfig := GetPrepackServerConfig(string(*pu.Implementation)) + + if pu.Endpoint.Type == REST { + c.Image = ServerConfig.RestConfig.ContainerImage + ":" + ServerConfig.RestConfig.DefaultImageVersion + } else { + c.Image = ServerConfig.GrpcConfig.ContainerImage + ":" + ServerConfig.GrpcConfig.DefaultImageVersion + } + + } +} + +// ----- + +func addDefaultsToGraph(pu *PredictiveUnit) { + if pu.Type == nil { + ty := UNKNOWN_TYPE + pu.Type = &ty + } + if pu.Implementation == nil { + im := UNKNOWN_IMPLEMENTATION + pu.Implementation = &im + } + for i := 0; i < len(pu.Children); i++ { + addDefaultsToGraph(&pu.Children[i]) + } +} + +func getUpdatePortNumMap(name string, nextPortNum *int32, portMap map[string]int32) int32 { + if _, present := portMap[name]; !present { + portMap[name] = *nextPortNum + *nextPortNum++ + } + return portMap[name] +} + +func (r *SeldonDeployment) DefaultSeldonDeployment() { + + var firstPuPortNum int32 = 9000 + if env_preditive_unit_service_port, ok := os.LookupEnv("PREDICTIVE_UNIT_SERVICE_PORT"); ok { + portNum, err := strconv.Atoi(env_preditive_unit_service_port) + if err != nil { + seldondeploymentlog.Error(err, "Failed to decode PREDICTIVE_UNIT_SERVICE_PORT will use default 9000", "value", env_preditive_unit_service_port) + } else { + firstPuPortNum = int32(portNum) + } + } + nextPortNum := firstPuPortNum + + portMap := map[string]int32{} + + if r.ObjectMeta.Namespace == "" { + r.ObjectMeta.Namespace = "default" + } + + for i := 0; i < len(r.Spec.Predictors); i++ { + p := r.Spec.Predictors[i] + if p.Graph.Type == nil { + ty := UNKNOWN_TYPE + p.Graph.Type = &ty + } + // Add version label for predictor if not present + if p.Labels == nil { + p.Labels = map[string]string{} + } + if _, present := p.Labels["version"]; !present { + p.Labels["version"] = p.Name + } + addDefaultsToGraph(p.Graph) + + r.Spec.Predictors[i] = p + + for j := 0; j < len(p.ComponentSpecs); j++ { + cSpec := r.Spec.Predictors[i].ComponentSpecs[j] + + // add service details for each container - looping this way as if containers in same pod and its the engine pod both need to be localhost + for k := 0; k < len(cSpec.Spec.Containers); k++ { + con := &cSpec.Spec.Containers[k] + + getUpdatePortNumMap(con.Name, &nextPortNum, portMap) + + portNum := portMap[con.Name] + + pu := GetPredictiveUnit(p.Graph, con.Name) + + if pu != nil { + + if pu.Endpoint == nil { + pu.Endpoint = &Endpoint{Type: REST} + } + var portType string + if pu.Endpoint.Type == GRPC { + portType = "grpc" + } else { + portType = "http" + } + + if con != nil { + existingPort := GetPort(portType, con.Ports) + if existingPort != nil { + portNum = existingPort.ContainerPort + } + + volFound := false + for _, vol := range con.VolumeMounts { + if vol.Name == PODINFO_VOLUME_NAME { + volFound = true + } + } + if !volFound { + con.VolumeMounts = append(con.VolumeMounts, corev1.VolumeMount{ + Name: PODINFO_VOLUME_NAME, + MountPath: PODINFO_VOLUME_PATH, + }) + } + } + + // Set ports and hostname in predictive unit so engine can read it from SDep + // if this is the first componentSpec then it's the one to put the engine in - note using outer loop counter here + if _, hasSeparateEnginePod := r.Spec.Annotations[ANNOTATION_SEPARATE_ENGINE]; j == 0 && !hasSeparateEnginePod { + pu.Endpoint.ServiceHost = "localhost" + } else { + containerServiceValue := GetContainerServiceName(r, p, con) + pu.Endpoint.ServiceHost = containerServiceValue + "." + r.ObjectMeta.Namespace + ".svc.cluster.local." + } + pu.Endpoint.ServicePort = portNum + } + } + + // Add defaultMode to volumes if not set to ensure no changes when comparing later in controller + for k := 0; k < len(cSpec.Spec.Volumes); k++ { + vol := &cSpec.Spec.Volumes[k] + if vol.Secret != nil && vol.Secret.DefaultMode == nil { + var defaultMode = corev1.SecretVolumeSourceDefaultMode + vol.Secret.DefaultMode = &defaultMode + } else if vol.ConfigMap != nil && vol.ConfigMap.DefaultMode == nil { + var defaultMode = corev1.ConfigMapVolumeSourceDefaultMode + vol.ConfigMap.DefaultMode = &defaultMode + } else if vol.DownwardAPI != nil && vol.DownwardAPI.DefaultMode == nil { + var defaultMode = corev1.DownwardAPIVolumeSourceDefaultMode + vol.DownwardAPI.DefaultMode = &defaultMode + } else if vol.Projected != nil && vol.Projected.DefaultMode == nil { + var defaultMode = corev1.ProjectedVolumeSourceDefaultMode + vol.Projected.DefaultMode = &defaultMode + } + } + } + + pus := GetPredictiveUnitList(p.Graph) + + //some pus might not have a container spec so pick those up + for l := 0; l < len(pus); l++ { + pu := pus[l] + + if IsPrepack(pu) { + + con := GetContainerForPredictiveUnit(&p, pu.Name) + + existing := con != nil + if !existing { + con = &corev1.Container{ + Name: pu.Name, + VolumeMounts: []corev1.VolumeMount{ + { + Name: PODINFO_VOLUME_NAME, + MountPath: PODINFO_VOLUME_PATH, + }, + }, + } + } + + // Add a default REST endpoint if none provided + // pu needs to have an endpoint as engine reads it from SDep in order to direct graph traffic + // probes etc will be added later by controller + if pu.Endpoint == nil { + pu.Endpoint = &Endpoint{Type: REST} + } + var portType string + if pu.Endpoint.Type == GRPC { + portType = "grpc" + } else { + portType = "http" + } + + SetImageNameForPrepackContainer(pu, con) + + // if new Add container to componentSpecs + if !existing { + if len(p.ComponentSpecs) > 0 { + p.ComponentSpecs[0].Spec.Containers = append(p.ComponentSpecs[0].Spec.Containers, *con) + } else { + podSpec := SeldonPodSpec{ + Metadata: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{*con}, + }, + } + p.ComponentSpecs = []*SeldonPodSpec{&podSpec} + + // p is a copy so update the entry + r.Spec.Predictors[i] = p + } + } + + getUpdatePortNumMap(con.Name, &nextPortNum, portMap) + portNum := portMap[pu.Name] + + if con != nil { + existingPort := GetPort(portType, con.Ports) + if existingPort != nil { + portNum = existingPort.ContainerPort + } + + volFound := false + for _, vol := range con.VolumeMounts { + if vol.Name == PODINFO_VOLUME_NAME { + volFound = true + } + } + if !volFound { + con.VolumeMounts = append(con.VolumeMounts, corev1.VolumeMount{ + Name: PODINFO_VOLUME_NAME, + MountPath: PODINFO_VOLUME_PATH, + }) + } + } + // Set ports and hostname in predictive unit so engine can read it from SDep + // if this is the firstPuPortNum then we've not added engine yet so put the engine in here + if pu.Endpoint.ServiceHost == "" { + if _, hasSeparateEnginePod := r.Spec.Annotations[ANNOTATION_SEPARATE_ENGINE]; !hasSeparateEnginePod { + pu.Endpoint.ServiceHost = "localhost" + } else { + containerServiceValue := GetContainerServiceName(r, p, con) + pu.Endpoint.ServiceHost = containerServiceValue + "." + r.ObjectMeta.Namespace + ".svc.cluster.local." + } + } + if pu.Endpoint.ServicePort == 0 { + pu.Endpoint.ServicePort = portNum + } + + } + + } + + } + +} + +// ----- + +// --- Validating + +// Check the predictive units to ensure the graph matches up with defined containers. +func checkPredictiveUnits(pu *PredictiveUnit, p *PredictorSpec, fldPath *field.Path, allErrs field.ErrorList) field.ErrorList { + if *pu.Implementation == UNKNOWN_IMPLEMENTATION { + + if GetContainerForPredictiveUnit(p, pu.Name) == nil { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "Can't find container for Predictive Unit")) + } + + if *pu.Type == UNKNOWN_TYPE && (pu.Methods == nil || len(*pu.Methods) == 0) { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "Predictive Unit has no implementation methods defined. Change to a known type or add what methods it defines")) + } + + } else if IsPrepack(pu) { + if pu.ModelURI == "" { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "Predictive unit modelUri required when using standalone servers")) + } + c := GetContainerForPredictiveUnit(p, pu.Name) + + if c == nil || c.Image == "" { + + ServersConfigs, err := getPredictorServerConfigs() + + if err != nil { + seldondeploymentlog.Error(err, "Failed to read prepacked model servers from configmap") + } + + _, ok := ServersConfigs[string(*pu.Implementation)] + if !ok { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "No entry in predictors map for "+string(*pu.Implementation))) + } + } + } + + for i := 0; i < len(pu.Children); i++ { + allErrs = checkPredictiveUnits(&pu.Children[i], p, fldPath.Index(i), allErrs) + } + + return allErrs +} + +func checkTraffic(mlDep *SeldonDeployment, fldPath *field.Path, allErrs field.ErrorList) field.ErrorList { + var trafficSum int32 = 0 + var shadows int = 0 + for i := 0; i < len(mlDep.Spec.Predictors); i++ { + p := mlDep.Spec.Predictors[i] + trafficSum = trafficSum + p.Traffic + + if p.Shadow == true { + shadows += 1 + } + } + if trafficSum != 100 && (len(mlDep.Spec.Predictors)-shadows) > 1 { + allErrs = append(allErrs, field.Invalid(fldPath, mlDep.Name, "Traffic must sum to 100 for multiple predictors")) + } + if trafficSum > 0 && trafficSum < 100 && len(mlDep.Spec.Predictors) == 1 { + allErrs = append(allErrs, field.Invalid(fldPath, mlDep.Name, "Traffic must sum be 100 for a single predictor when set")) + } + + 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")) + } + predictorNames[p.Name] = true + allErrs = checkPredictiveUnits(p.Graph, &p, field.NewPath("spec").Child("predictors").Index(i).Child("graph"), allErrs) + } + + allErrs = checkTraffic(r, field.NewPath("spec"), allErrs) + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + schema.GroupKind{Group: "machinelearing.seldon.io", Kind: "SeldonDeployment"}, + r.Name, allErrs) + +} + +/// --- + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-machinelearning-seldon-io-v1-seldondeployment,mutating=true,failurePolicy=fail,groups=machinelearning.seldon.io,resources=seldondeployments,verbs=create;update,versions=v1,name=mseldondeployment.kb.io + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *SeldonDeployment) Default() { + seldondeploymentlog.Info("Defaulting web hook called", "name", r.Name) + + r.DefaultSeldonDeployment() +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// +kubebuilder:webhook:verbs=create;update,path=/validate-machinelearning-seldon-io-v1-seldondeployment,mutating=false,failurePolicy=fail,groups=machinelearning.seldon.io,resources=seldondeployments,versions=v1,name=vseldondeployment.kb.io + +var _ webhook.Validator = &SeldonDeployment{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *SeldonDeployment) ValidateCreate() error { + seldondeploymentlog.Info("Validating Webhook called for CREATE", "name", r.Name) + + return r.validateSeldonDeployment() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *SeldonDeployment) ValidateUpdate(old runtime.Object) error { + seldondeploymentlog.Info("Validating webhook called for UPDATE", "name", r.Name) + + return r.validateSeldonDeployment() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *SeldonDeployment) ValidateDelete() error { + seldondeploymentlog.Info("Validating webhook called for DELETE", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil +} diff --git a/operator/api/v1/zz_generated.deepcopy.go b/operator/api/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..ff753c2707 --- /dev/null +++ b/operator/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,443 @@ +// +build !ignore_autogenerated + +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + "k8s.io/api/autoscaling/v2beta1" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatus. +func (in *DeploymentStatus) DeepCopy() *DeploymentStatus { + if in == nil { + return nil + } + out := new(DeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Endpoint) DeepCopyInto(out *Endpoint) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint. +func (in *Endpoint) DeepCopy() *Endpoint { + if in == nil { + return nil + } + out := new(Endpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Explainer) DeepCopyInto(out *Explainer) { + *out = *in + in.ContainerSpec.DeepCopyInto(&out.ContainerSpec) + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Endpoint != nil { + in, out := &in.Endpoint, &out.Endpoint + *out = new(Endpoint) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Explainer. +func (in *Explainer) DeepCopy() *Explainer { + if in == nil { + return nil + } + out := new(Explainer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictiveUnit) DeepCopyInto(out *PredictiveUnit) { + *out = *in + if in.Children != nil { + in, out := &in.Children, &out.Children + *out = make([]PredictiveUnit, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(PredictiveUnitType) + **out = **in + } + if in.Implementation != nil { + in, out := &in.Implementation, &out.Implementation + *out = new(PredictiveUnitImplementation) + **out = **in + } + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = new([]PredictiveUnitMethod) + if **in != nil { + in, out := *in, *out + *out = make([]PredictiveUnitMethod, len(*in)) + copy(*out, *in) + } + } + if in.Endpoint != nil { + in, out := &in.Endpoint, &out.Endpoint + *out = new(Endpoint) + **out = **in + } + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictiveUnit. +func (in *PredictiveUnit) DeepCopy() *PredictiveUnit { + if in == nil { + return nil + } + out := new(PredictiveUnit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictorImageConfig) DeepCopyInto(out *PredictorImageConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictorImageConfig. +func (in *PredictorImageConfig) DeepCopy() *PredictorImageConfig { + if in == nil { + return nil + } + out := new(PredictorImageConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictorServerConfig) DeepCopyInto(out *PredictorServerConfig) { + *out = *in + out.RestConfig = in.RestConfig + out.GrpcConfig = in.GrpcConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictorServerConfig. +func (in *PredictorServerConfig) DeepCopy() *PredictorServerConfig { + if in == nil { + return nil + } + out := new(PredictorServerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictorSpec) DeepCopyInto(out *PredictorSpec) { + *out = *in + if in.Graph != nil { + in, out := &in.Graph, &out.Graph + *out = new(PredictiveUnit) + (*in).DeepCopyInto(*out) + } + if in.ComponentSpecs != nil { + in, out := &in.ComponentSpecs, &out.ComponentSpecs + *out = make([]*SeldonPodSpec, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(SeldonPodSpec) + (*in).DeepCopyInto(*out) + } + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.EngineResources.DeepCopyInto(&out.EngineResources) + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.SvcOrchSpec.DeepCopyInto(&out.SvcOrchSpec) + in.Explainer.DeepCopyInto(&out.Explainer) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictorSpec. +func (in *PredictorSpec) DeepCopy() *PredictorSpec { + if in == nil { + return nil + } + out := new(PredictorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeployment) DeepCopyInto(out *SeldonDeployment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeployment. +func (in *SeldonDeployment) DeepCopy() *SeldonDeployment { + if in == nil { + return nil + } + out := new(SeldonDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeldonDeployment) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeploymentList) DeepCopyInto(out *SeldonDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SeldonDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeploymentList. +func (in *SeldonDeploymentList) DeepCopy() *SeldonDeploymentList { + if in == nil { + return nil + } + out := new(SeldonDeploymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeldonDeploymentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeploymentSpec) DeepCopyInto(out *SeldonDeploymentSpec) { + *out = *in + if in.Predictors != nil { + in, out := &in.Predictors, &out.Predictors + *out = make([]PredictorSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeploymentSpec. +func (in *SeldonDeploymentSpec) DeepCopy() *SeldonDeploymentSpec { + if in == nil { + return nil + } + out := new(SeldonDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeploymentStatus) DeepCopyInto(out *SeldonDeploymentStatus) { + *out = *in + if in.DeploymentStatus != nil { + in, out := &in.DeploymentStatus, &out.DeploymentStatus + *out = make(map[string]DeploymentStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ServiceStatus != nil { + in, out := &in.ServiceStatus, &out.ServiceStatus + *out = make(map[string]ServiceStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeploymentStatus. +func (in *SeldonDeploymentStatus) DeepCopy() *SeldonDeploymentStatus { + if in == nil { + return nil + } + out := new(SeldonDeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonHpaSpec) DeepCopyInto(out *SeldonHpaSpec) { + *out = *in + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int32) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]v2beta1.MetricSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonHpaSpec. +func (in *SeldonHpaSpec) DeepCopy() *SeldonHpaSpec { + if in == nil { + return nil + } + out := new(SeldonHpaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonPodSpec) DeepCopyInto(out *SeldonPodSpec) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + in.Spec.DeepCopyInto(&out.Spec) + if in.HpaSpec != nil { + in, out := &in.HpaSpec, &out.HpaSpec + *out = new(SeldonHpaSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonPodSpec. +func (in *SeldonPodSpec) DeepCopy() *SeldonPodSpec { + if in == nil { + return nil + } + out := new(SeldonPodSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceStatus. +func (in *ServiceStatus) DeepCopy() *ServiceStatus { + if in == nil { + return nil + } + out := new(ServiceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SvcOrchSpec) DeepCopyInto(out *SvcOrchSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]*v1.EnvVar, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(v1.EnvVar) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SvcOrchSpec. +func (in *SvcOrchSpec) DeepCopy() *SvcOrchSpec { + if in == nil { + return nil + } + out := new(SvcOrchSpec) + in.DeepCopyInto(out) + return out +} From b8d5ac3b456b2a1a1f7fa50d0dc34f80e59929d8 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Tue, 10 Dec 2019 14:33:09 +0000 Subject: [PATCH 02/12] Point imports to v1 --- operator/api/v1/zz_generated.deepcopy.go | 8 +- operator/controllers/ambassador.go | 12 +- operator/controllers/ambassador_test.go | 12 +- operator/controllers/controller_utils.go | 14 +- operator/controllers/noengine_test.go | 18 +-- .../seldondeployment_controller.go | 138 +++++++++--------- .../seldondeployment_controller_test.go | 18 +-- .../controllers/seldondeployment_engine.go | 42 +++--- .../seldondeployment_explainers.go | 40 ++--- .../seldondeployment_prepackaged_servers.go | 64 ++++---- ...ldondeployment_prepackaged_servers_test.go | 20 +-- operator/controllers/suite_test.go | 10 +- operator/main.go | 6 +- operator/utils/utils.go | 6 +- 14 files changed, 204 insertions(+), 204 deletions(-) diff --git a/operator/api/v1/zz_generated.deepcopy.go b/operator/api/v1/zz_generated.deepcopy.go index ff753c2707..1e4b16e384 100644 --- a/operator/api/v1/zz_generated.deepcopy.go +++ b/operator/api/v1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1 import ( "k8s.io/api/autoscaling/v2beta1" - "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -416,16 +416,16 @@ func (in *SvcOrchSpec) DeepCopyInto(out *SvcOrchSpec) { *out = *in if in.Resources != nil { in, out := &in.Resources, &out.Resources - *out = new(v1.ResourceRequirements) + *out = new(corev1.ResourceRequirements) (*in).DeepCopyInto(*out) } if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]*v1.EnvVar, len(*in)) + *out = make([]*corev1.EnvVar, len(*in)) for i := range *in { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] - *out = new(v1.EnvVar) + *out = new(corev1.EnvVar) (*in).DeepCopyInto(*out) } } diff --git a/operator/controllers/ambassador.go b/operator/controllers/ambassador.go index 1affc0e51a..4442e6726b 100644 --- a/operator/controllers/ambassador.go +++ b/operator/controllers/ambassador.go @@ -1,7 +1,7 @@ package controllers import ( - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "gopkg.in/yaml.v2" "strconv" "strings" @@ -47,8 +47,8 @@ type AmbassadorRetryPolicy struct { } // Return a REST configuration for Ambassador with optional custom settings. -func getAmbassadorRestConfig(mlDep *machinelearningv1alpha2.SeldonDeployment, - p *machinelearningv1alpha2.PredictorSpec, +func getAmbassadorRestConfig(mlDep *machinelearningv1.SeldonDeployment, + p *machinelearningv1.PredictorSpec, addNamespace bool, serviceName string, serviceNameExternal string, @@ -131,8 +131,8 @@ func getAmbassadorRestConfig(mlDep *machinelearningv1alpha2.SeldonDeployment, } // Return a gRPC configuration for Ambassador with optional custom settings. -func getAmbassadorGrpcConfig(mlDep *machinelearningv1alpha2.SeldonDeployment, - p *machinelearningv1alpha2.PredictorSpec, +func getAmbassadorGrpcConfig(mlDep *machinelearningv1.SeldonDeployment, + p *machinelearningv1.PredictorSpec, addNamespace bool, serviceName string, serviceNameExternal string, @@ -219,7 +219,7 @@ func getAmbassadorGrpcConfig(mlDep *machinelearningv1alpha2.SeldonDeployment, // Get the configuration for ambassador using the servce name serviceName. // Up to 4 confgurations will be created covering REST, GRPC and cluster-wide and namespaced varieties. // Annotations for Ambassador will be used to customize the configuration returned. -func getAmbassadorConfigs(mlDep *machinelearningv1alpha2.SeldonDeployment, p *machinelearningv1alpha2.PredictorSpec, serviceName string, engine_http_port, engine_grpc_port int, nameOverride string) (string, error) { +func getAmbassadorConfigs(mlDep *machinelearningv1.SeldonDeployment, p *machinelearningv1.PredictorSpec, serviceName string, engine_http_port, engine_grpc_port int, nameOverride string) (string, error) { if annotation := getAnnotation(mlDep, ANNOTATION_AMBASSADOR_CUSTOM, ""); annotation != "" { return annotation, nil } else { diff --git a/operator/controllers/ambassador_test.go b/operator/controllers/ambassador_test.go index d3595513c0..eccf441ce3 100644 --- a/operator/controllers/ambassador_test.go +++ b/operator/controllers/ambassador_test.go @@ -1,7 +1,7 @@ package controllers import ( - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "strings" @@ -9,8 +9,8 @@ import ( ) func TestAmbassadorBasic(t *testing.T) { - mlDep := machinelearningv1alpha2.SeldonDeployment{ObjectMeta: metav1.ObjectMeta{Name: "mymodel"}} - p := machinelearningv1alpha2.PredictorSpec{Name: "p"} + mlDep := machinelearningv1.SeldonDeployment{ObjectMeta: metav1.ObjectMeta{Name: "mymodel"}} + p := machinelearningv1.PredictorSpec{Name: "p"} s, err := getAmbassadorConfigs(&mlDep, &p, "myservice", 9000, 5000, "") if err != nil { t.Fatalf("Config format error") @@ -46,9 +46,9 @@ func TestAmbassadorBasic(t *testing.T) { } func TestAmbassadorID(t *testing.T) { - mlDep := machinelearningv1alpha2.SeldonDeployment{ObjectMeta: metav1.ObjectMeta{Name: "mymodel"}, - Spec: machinelearningv1alpha2.SeldonDeploymentSpec{Annotations: map[string]string{ANNOTATION_AMBASSADOR_ID: "myinstance_id"}}} - p := machinelearningv1alpha2.PredictorSpec{Name: "p"} + mlDep := machinelearningv1.SeldonDeployment{ObjectMeta: metav1.ObjectMeta{Name: "mymodel"}, + Spec: machinelearningv1.SeldonDeploymentSpec{Annotations: map[string]string{ANNOTATION_AMBASSADOR_ID: "myinstance_id"}}} + p := machinelearningv1.PredictorSpec{Name: "p"} s, err := getAmbassadorConfigs(&mlDep, &p, "myservice", 9000, 5000, "") if err != nil { t.Fatalf("Config format error") diff --git a/operator/controllers/controller_utils.go b/operator/controllers/controller_utils.go index a8f7d7f17c..eee28213f7 100644 --- a/operator/controllers/controller_utils.go +++ b/operator/controllers/controller_utils.go @@ -3,7 +3,7 @@ package controllers import ( "encoding/base64" "encoding/json" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "os" @@ -12,7 +12,7 @@ import ( ) // Get the Namespace from the SeldonDeployment. Returns "default" if none found. -func getNamespace(deployment *machinelearningv1alpha2.SeldonDeployment) string { +func getNamespace(deployment *machinelearningv1.SeldonDeployment) string { if len(deployment.ObjectMeta.Namespace) > 0 { return deployment.ObjectMeta.Namespace } else { @@ -21,7 +21,7 @@ func getNamespace(deployment *machinelearningv1alpha2.SeldonDeployment) string { } // Translte the PredictorSpec p in to base64 encoded JSON to feed to engine in env var. -func getEngineVarJson(p *machinelearningv1alpha2.PredictorSpec) (string, error) { +func getEngineVarJson(p *machinelearningv1.PredictorSpec) (string, error) { pcopy := p.DeepCopy() // engine doesn't need to know about metadata or explainer @@ -29,7 +29,7 @@ func getEngineVarJson(p *machinelearningv1alpha2.PredictorSpec) (string, error) for _, compSpec := range pcopy.ComponentSpecs { compSpec.Metadata.CreationTimestamp = metav1.Time{} } - pcopy.Explainer = machinelearningv1alpha2.Explainer{} + pcopy.Explainer = machinelearningv1.Explainer{} str, err := json.Marshal(pcopy) if err != nil { @@ -47,7 +47,7 @@ func GetEnv(key, fallback string) string { } // Get an annotation from the Seldon Deployment given by annotationKey or return the fallback. -func getAnnotation(mlDep *machinelearningv1alpha2.SeldonDeployment, annotationKey string, fallback string) string { +func getAnnotation(mlDep *machinelearningv1.SeldonDeployment, annotationKey string, fallback string) string { if annotation, hasAnnotation := mlDep.Spec.Annotations[annotationKey]; hasAnnotation { return annotation } else { @@ -56,7 +56,7 @@ func getAnnotation(mlDep *machinelearningv1alpha2.SeldonDeployment, annotationKe } //get annotations that start with seldon.io/engine -func getEngineEnvAnnotations(mlDep *machinelearningv1alpha2.SeldonDeployment) []corev1.EnvVar { +func getEngineEnvAnnotations(mlDep *machinelearningv1.SeldonDeployment) []corev1.EnvVar { envVars := make([]corev1.EnvVar, 0) var keys []string @@ -67,7 +67,7 @@ func getEngineEnvAnnotations(mlDep *machinelearningv1alpha2.SeldonDeployment) [] for _, k := range keys { //prefix indicates engine annotation but "seldon.io/engine-separate-pod" isn't an env one - if strings.HasPrefix(k, "seldon.io/engine-") && k != machinelearningv1alpha2.ANNOTATION_SEPARATE_ENGINE { + if strings.HasPrefix(k, "seldon.io/engine-") && k != machinelearningv1.ANNOTATION_SEPARATE_ENGINE { name := strings.TrimPrefix(k, "seldon.io/engine-") var replacer = strings.NewReplacer("-", "_") name = replacer.Replace(name) diff --git a/operator/controllers/noengine_test.go b/operator/controllers/noengine_test.go index 9f503a80b0..8fc98fe018 100644 --- a/operator/controllers/noengine_test.go +++ b/operator/controllers/noengine_test.go @@ -4,7 +4,7 @@ import ( "context" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -18,25 +18,25 @@ var _ = Describe("Create a Seldon Deployment without engine", func() { By("Creating a resource") It("should create a resource with defaults", func() { Expect(k8sClient).NotTo(BeNil()) - var modelType = machinelearningv1alpha2.MODEL + var modelType = machinelearningv1.MODEL key := types.NamespacedName{ Name: "dep2", Namespace: "default", } - instance := &machinelearningv1alpha2.SeldonDeployment{ + instance := &machinelearningv1.SeldonDeployment{ ObjectMeta: metav1.ObjectMeta{ Name: key.Name, Namespace: key.Namespace, }, - Spec: machinelearningv1alpha2.SeldonDeploymentSpec{ + Spec: machinelearningv1.SeldonDeploymentSpec{ Name: "mydep2", - Predictors: []machinelearningv1alpha2.PredictorSpec{ + Predictors: []machinelearningv1.PredictorSpec{ { Annotations: map[string]string{ "seldon.io/no-engine": "true", }, Name: "p1", - ComponentSpecs: []*machinelearningv1alpha2.SeldonPodSpec{ + ComponentSpecs: []*machinelearningv1.SeldonPodSpec{ { Spec: v1.PodSpec{ Containers: []v1.Container{ @@ -48,7 +48,7 @@ var _ = Describe("Create a Seldon Deployment without engine", func() { }, }, }, - Graph: &machinelearningv1alpha2.PredictiveUnit{ + Graph: &machinelearningv1.PredictiveUnit{ Name: "classifier", Type: &modelType, }, @@ -63,7 +63,7 @@ var _ = Describe("Create a Seldon Deployment without engine", func() { Expect(k8sClient.Create(context.Background(), instance)).Should(Succeed()) //time.Sleep(time.Second * 5) - fetched := &machinelearningv1alpha2.SeldonDeployment{} + fetched := &machinelearningv1.SeldonDeployment{} Eventually(func() error { err := k8sClient.Get(context.Background(), key, fetched) return err @@ -71,7 +71,7 @@ var _ = Describe("Create a Seldon Deployment without engine", func() { Expect(fetched.Spec.Name).Should(Equal("mydep2")) depKey := types.NamespacedName{ - Name: machinelearningv1alpha2.GetDeploymentName(instance, instance.Spec.Predictors[0], instance.Spec.Predictors[0].ComponentSpecs[0]), + Name: machinelearningv1.GetDeploymentName(instance, instance.Spec.Predictors[0], instance.Spec.Predictors[0].ComponentSpecs[0]), Namespace: "default", } depFetched := &appsv1.Deployment{} diff --git a/operator/controllers/seldondeployment_controller.go b/operator/controllers/seldondeployment_controller.go index 16f73be7f7..00df4b060b 100644 --- a/operator/controllers/seldondeployment_controller.go +++ b/operator/controllers/seldondeployment_controller.go @@ -37,7 +37,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "encoding/json" @@ -72,7 +72,7 @@ type SeldonDeploymentReconciler struct { //---------------- Old part type components struct { - serviceDetails map[string]*machinelearningv1alpha2.ServiceStatus + serviceDetails map[string]*machinelearningv1.ServiceStatus deployments []*appsv1.Deployment services []*corev1.Service hpas []*autoscaling.HorizontalPodAutoscaler @@ -92,12 +92,12 @@ type httpGrpcPorts struct { grpcPort int } -func createHpa(podSpec *machinelearningv1alpha2.SeldonPodSpec, deploymentName string, seldonId string, namespace string) *autoscaling.HorizontalPodAutoscaler { +func createHpa(podSpec *machinelearningv1.SeldonPodSpec, deploymentName string, seldonId string, namespace string) *autoscaling.HorizontalPodAutoscaler { hpa := autoscaling.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: deploymentName, Namespace: namespace, - Labels: map[string]string{machinelearningv1alpha2.Label_seldon_id: seldonId}, + Labels: map[string]string{machinelearningv1.Label_seldon_id: seldonId}, }, Spec: autoscaling.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscaling.CrossVersionObjectReference{ @@ -118,7 +118,7 @@ func createHpa(podSpec *machinelearningv1alpha2.SeldonPodSpec, deploymentName st // Create istio virtual service and destination rule. // Creates routes for each predictor with traffic weight split -func createIstioResources(mlDep *machinelearningv1alpha2.SeldonDeployment, +func createIstioResources(mlDep *machinelearningv1.SeldonDeployment, seldonId string, namespace string, ports []httpGrpcPorts, @@ -188,7 +188,7 @@ func createIstioResources(mlDep *machinelearningv1alpha2.SeldonDeployment, for i := 0; i < len(mlDep.Spec.Predictors); i++ { p := mlDep.Spec.Predictors[i] - pSvcName := machinelearningv1alpha2.GetPredictorKey(mlDep, &p) + pSvcName := machinelearningv1.GetPredictorKey(mlDep, &p) drule := &istio.DestinationRule{ ObjectMeta: metav1.ObjectMeta{ @@ -307,10 +307,10 @@ func getEngineGrpcPort() (engine_grpc_port int, err error) { } // Create all the components (Deployments, Services etc) -func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alpha2.SeldonDeployment, log logr.Logger) (*components, error) { +func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1.SeldonDeployment, log logr.Logger) (*components, error) { c := components{} - c.serviceDetails = map[string]*machinelearningv1alpha2.ServiceStatus{} - seldonId := machinelearningv1alpha2.GetSeldonDeploymentName(mlDep) + c.serviceDetails = map[string]*machinelearningv1.ServiceStatus{} + seldonId := machinelearningv1.GetSeldonDeploymentName(mlDep) namespace := getNamespace(mlDep) engine_http_port, err := getEngineHttpPort() @@ -332,14 +332,14 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp // 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] + _, noEngine := p.Annotations[machinelearningv1.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) + pu := machinelearningv1.GetPredictiveUnit(p.Graph, p.ComponentSpecs[0].Spec.Containers[0].Name) if pu != nil { - if pu.Endpoint != nil && pu.Endpoint.Type == machinelearningv1alpha2.GRPC { + if pu.Endpoint != nil && pu.Endpoint.Type == machinelearningv1.GRPC { httpAllowed = false } - if pu.Endpoint == nil || pu.Endpoint.Type == machinelearningv1alpha2.REST { + if pu.Endpoint == nil || pu.Endpoint.Type == machinelearningv1.REST { grpcAllowed = false } } @@ -348,11 +348,11 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp 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) + _, noEngine := p.Annotations[machinelearningv1.ANNOTATION_NO_ENGINE] + pSvcName := machinelearningv1.GetPredictorKey(mlDep, &p) log.Info("pSvcName", "val", pSvcName) // Add engine deployment if separate - _, hasSeparateEnginePod := mlDep.Spec.Annotations[machinelearningv1alpha2.ANNOTATION_SEPARATE_ENGINE] + _, hasSeparateEnginePod := mlDep.Spec.Annotations[machinelearningv1.ANNOTATION_SEPARATE_ENGINE] if hasSeparateEnginePod && !noEngine { deploy, err := createEngineDeployment(mlDep, &p, pSvcName, engine_http_port, engine_grpc_port) if err != nil { @@ -370,7 +370,7 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp } // create Deployment from podspec - depName := machinelearningv1alpha2.GetDeploymentName(mlDep, p, cSpec) + depName := machinelearningv1.GetDeploymentName(mlDep, p, cSpec) deploy := createDeploymentWithoutEngine(depName, seldonId, cSpec, &p, mlDep) // Add HPA if needed @@ -402,9 +402,9 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp } 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 + deploy.ObjectMeta.Labels[machinelearningv1.Label_seldon_app] = pSvcName + deploy.Spec.Selector.MatchLabels[machinelearningv1.Label_seldon_app] = pSvcName + deploy.Spec.Template.ObjectMeta.Labels[machinelearningv1.Label_seldon_app] = pSvcName port := int(svc.Spec.Ports[0].Port) @@ -418,7 +418,7 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp c.services = append(c.services, psvc) - c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + c.serviceDetails[pSvcName] = &machinelearningv1.ServiceStatus{ SvcName: pSvcName, GrpcEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(port), } @@ -432,7 +432,7 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp c.services = append(c.services, psvc) - c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + c.serviceDetails[pSvcName] = &machinelearningv1.ServiceStatus{ SvcName: pSvcName, HttpEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(port), } @@ -457,7 +457,7 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp found := false // find the pu that the webhook marked as localhost as its corresponding deployment should get the engine - pu := machinelearningv1alpha2.GetEnginePredictiveUnit(p.Graph) + pu := machinelearningv1.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") @@ -501,18 +501,18 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp c.services = append(c.services, psvc) if httpAllowed && grpcAllowed { - c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + c.serviceDetails[pSvcName] = &machinelearningv1.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{ + c.serviceDetails[pSvcName] = &machinelearningv1.ServiceStatus{ SvcName: pSvcName, HttpEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_http_port), } } else if grpcAllowed { - c.serviceDetails[pSvcName] = &machinelearningv1alpha2.ServiceStatus{ + c.serviceDetails[pSvcName] = &machinelearningv1.ServiceStatus{ SvcName: pSvcName, GrpcEndpoint: pSvcName + "." + namespace + ":" + strconv.Itoa(engine_grpc_port), } @@ -537,8 +537,8 @@ func createComponents(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alp } //Creates Service for Predictor - exposed externally (ambassador or istio) -func createPredictorService(pSvcName string, seldonId string, p *machinelearningv1alpha2.PredictorSpec, - mlDep *machinelearningv1alpha2.SeldonDeployment, +func createPredictorService(pSvcName string, seldonId string, p *machinelearningv1.PredictorSpec, + mlDep *machinelearningv1.SeldonDeployment, engine_http_port int, engine_grpc_port int, ambassadorNameOverride string, @@ -549,11 +549,11 @@ func createPredictorService(pSvcName string, seldonId string, p *machinelearning ObjectMeta: metav1.ObjectMeta{ Name: pSvcName, Namespace: namespace, - Labels: map[string]string{machinelearningv1alpha2.Label_seldon_app: pSvcName, - machinelearningv1alpha2.Label_seldon_id: seldonId}, + Labels: map[string]string{machinelearningv1.Label_seldon_app: pSvcName, + machinelearningv1.Label_seldon_id: seldonId}, }, Spec: corev1.ServiceSpec{ - Selector: map[string]string{machinelearningv1alpha2.Label_seldon_app: pSvcName}, + Selector: map[string]string{machinelearningv1.Label_seldon_app: pSvcName}, SessionAffinity: corev1.ServiceAffinityNone, Type: corev1.ServiceTypeClusterIP, }, @@ -577,7 +577,7 @@ func createPredictorService(pSvcName string, seldonId string, p *machinelearning psvc.Annotations[AMBASSADOR_ANNOTATION] = ambassadorConfig } - if getAnnotation(mlDep, machinelearningv1alpha2.ANNOTATION_HEADLESS_SVC, "false") != "false" { + if getAnnotation(mlDep, machinelearningv1.ANNOTATION_HEADLESS_SVC, "false") != "false" { log.Info("Creating Headless SVC") psvc.Spec.ClusterIP = "None" } @@ -586,10 +586,10 @@ func createPredictorService(pSvcName string, seldonId string, p *machinelearning } // service for hitting a model directly, not via engine - not exposed externally, also adds probes -func createContainerService(deploy *appsv1.Deployment, p machinelearningv1alpha2.PredictorSpec, mlDep *machinelearningv1alpha2.SeldonDeployment, con *corev1.Container, c components) *corev1.Service { - containerServiceKey := machinelearningv1alpha2.GetPredictorServiceNameKey(con) - containerServiceValue := machinelearningv1alpha2.GetContainerServiceName(mlDep, p, con) - pu := machinelearningv1alpha2.GetPredictiveUnit(p.Graph, con.Name) +func createContainerService(deploy *appsv1.Deployment, p machinelearningv1.PredictorSpec, mlDep *machinelearningv1.SeldonDeployment, con *corev1.Container, c components) *corev1.Service { + containerServiceKey := machinelearningv1.GetPredictorServiceNameKey(con) + containerServiceValue := machinelearningv1.GetContainerServiceName(mlDep, p, con) + pu := machinelearningv1.GetPredictiveUnit(p.Graph, con.Name) // only create services for containers defined as pus in the graph if pu == nil { @@ -599,12 +599,12 @@ func createContainerService(deploy *appsv1.Deployment, p machinelearningv1alpha2 portType := "http" var portNum int32 portNum = 0 - existingPort := machinelearningv1alpha2.GetPort(portType, con.Ports) + existingPort := machinelearningv1.GetPort(portType, con.Ports) if existingPort != nil { portNum = existingPort.ContainerPort } - if pu.Endpoint.Type == machinelearningv1alpha2.GRPC { + if pu.Endpoint.Type == machinelearningv1.GRPC { portType = "grpc" } @@ -621,11 +621,11 @@ func createContainerService(deploy *appsv1.Deployment, p machinelearningv1alpha2 } if portType == "grpc" { - c.serviceDetails[containerServiceValue] = &machinelearningv1alpha2.ServiceStatus{ + c.serviceDetails[containerServiceValue] = &machinelearningv1.ServiceStatus{ SvcName: containerServiceValue, GrpcEndpoint: containerServiceValue + "." + namespace + ":" + strconv.Itoa(int(portNum))} } else { - c.serviceDetails[containerServiceValue] = &machinelearningv1alpha2.ServiceStatus{ + c.serviceDetails[containerServiceValue] = &machinelearningv1.ServiceStatus{ SvcName: containerServiceValue, HttpEndpoint: containerServiceValue + "." + namespace + ":" + strconv.Itoa(int(portNum))} } @@ -634,7 +634,7 @@ func createContainerService(deploy *appsv1.Deployment, p machinelearningv1alpha2 ObjectMeta: metav1.ObjectMeta{ Name: containerServiceValue, Namespace: namespace, - Labels: map[string]string{containerServiceKey: containerServiceValue, machinelearningv1alpha2.Label_seldon_id: mlDep.Spec.Name}, + Labels: map[string]string{containerServiceKey: containerServiceValue, machinelearningv1.Label_seldon_id: mlDep.Spec.Name}, }, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ @@ -673,38 +673,38 @@ func createContainerService(deploy *appsv1.Deployment, p machinelearningv1alpha2 } // Add Environment Variables - if !utils.HasEnvVar(con.Env, machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_SERVICE_PORT) { + if !utils.HasEnvVar(con.Env, machinelearningv1.ENV_PREDICTIVE_UNIT_SERVICE_PORT) { con.Env = append(con.Env, []corev1.EnvVar{ - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_SERVICE_PORT, Value: strconv.Itoa(int(portNum))}, - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_ID, Value: con.Name}, - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTOR_ID, Value: p.Name}, - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_SELDON_DEPLOYMENT_ID, Value: mlDep.ObjectMeta.Name}, + corev1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_SERVICE_PORT, Value: strconv.Itoa(int(portNum))}, + corev1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_ID, Value: con.Name}, + corev1.EnvVar{Name: machinelearningv1.ENV_PREDICTOR_ID, Value: p.Name}, + corev1.EnvVar{Name: machinelearningv1.ENV_SELDON_DEPLOYMENT_ID, Value: mlDep.ObjectMeta.Name}, }...) } if pu != nil && len(pu.Parameters) > 0 { - if !utils.HasEnvVar(con.Env, machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS) { - con.Env = append(con.Env, corev1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: utils.GetPredictiveUnitAsJson(pu.Parameters)}) + if !utils.HasEnvVar(con.Env, machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS) { + con.Env = append(con.Env, corev1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: utils.GetPredictiveUnitAsJson(pu.Parameters)}) } } return svc } -func createDeploymentWithoutEngine(depName string, seldonId string, seldonPodSpec *machinelearningv1alpha2.SeldonPodSpec, p *machinelearningv1alpha2.PredictorSpec, mlDep *machinelearningv1alpha2.SeldonDeployment) *appsv1.Deployment { +func createDeploymentWithoutEngine(depName string, seldonId string, seldonPodSpec *machinelearningv1.SeldonPodSpec, p *machinelearningv1.PredictorSpec, mlDep *machinelearningv1.SeldonDeployment) *appsv1.Deployment { deploy := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: depName, Namespace: getNamespace(mlDep), - Labels: map[string]string{machinelearningv1alpha2.Label_seldon_id: seldonId, "app": depName, "fluentd": "true"}, + Labels: map[string]string{machinelearningv1.Label_seldon_id: seldonId, "app": depName, "fluentd": "true"}, }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{machinelearningv1alpha2.Label_seldon_id: seldonId}, + MatchLabels: map[string]string{machinelearningv1.Label_seldon_id: seldonId}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{machinelearningv1alpha2.Label_seldon_id: seldonId, "app": depName, "fluentd": "true"}, + Labels: map[string]string{machinelearningv1.Label_seldon_id: seldonId, "app": depName, "fluentd": "true"}, Annotations: mlDep.Spec.Annotations, }, }, @@ -748,7 +748,7 @@ func createDeploymentWithoutEngine(depName string, seldonId string, seldonPodSpe volFound := false for _, vol := range deploy.Spec.Template.Spec.Volumes { - if vol.Name == machinelearningv1alpha2.PODINFO_VOLUME_NAME { + if vol.Name == machinelearningv1.PODINFO_VOLUME_NAME { volFound = true } } @@ -756,7 +756,7 @@ func createDeploymentWithoutEngine(depName string, seldonId string, seldonPodSpe if !volFound { var defaultMode = corev1.DownwardAPIVolumeSourceDefaultMode //Add downwardAPI - deploy.Spec.Template.Spec.Volumes = append(deploy.Spec.Template.Spec.Volumes, corev1.Volume{Name: machinelearningv1alpha2.PODINFO_VOLUME_NAME, VolumeSource: corev1.VolumeSource{ + deploy.Spec.Template.Spec.Volumes = append(deploy.Spec.Template.Spec.Volumes, corev1.Volume{Name: machinelearningv1.PODINFO_VOLUME_NAME, VolumeSource: corev1.VolumeSource{ DownwardAPI: &corev1.DownwardAPIVolumeSource{Items: []corev1.DownwardAPIVolumeFile{ {Path: "annotations", FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.annotations", APIVersion: "v1"}}}, DefaultMode: &defaultMode}}}) } @@ -774,7 +774,7 @@ func getPort(name string, ports []corev1.ContainerPort) *corev1.ContainerPort { } // Create Services specified in components. -func createIstioServices(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1alpha2.SeldonDeployment, log logr.Logger) (bool, error) { +func createIstioServices(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1.SeldonDeployment, log logr.Logger) (bool, error) { ready := true for _, svc := range components.virtualServices { if err := controllerutil.SetControllerReference(instance, svc, r.Scheme); err != nil { @@ -821,7 +821,7 @@ func createIstioServices(r *SeldonDeploymentReconciler, components *components, log.Info("Found identical Virtual Service", "namespace", found.Namespace, "name", found.Name) if instance.Status.ServiceStatus == nil { - instance.Status.ServiceStatus = map[string]machinelearningv1alpha2.ServiceStatus{} + instance.Status.ServiceStatus = map[string]machinelearningv1.ServiceStatus{} } /* @@ -884,7 +884,7 @@ func createIstioServices(r *SeldonDeploymentReconciler, components *components, log.Info("Found identical Istio Destination Rule", "namespace", found.Namespace, "name", found.Name) if instance.Status.ServiceStatus == nil { - instance.Status.ServiceStatus = map[string]machinelearningv1alpha2.ServiceStatus{} + instance.Status.ServiceStatus = map[string]machinelearningv1.ServiceStatus{} } if _, ok := instance.Status.ServiceStatus[found.Name]; !ok { @@ -903,7 +903,7 @@ func createIstioServices(r *SeldonDeploymentReconciler, components *components, } // Create Services specified in components. -func createServices(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1alpha2.SeldonDeployment, all bool, log logr.Logger) (bool, error) { +func createServices(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1.SeldonDeployment, all bool, log logr.Logger) (bool, error) { ready := true for _, svc := range components.services { if !all { @@ -959,7 +959,7 @@ func createServices(r *SeldonDeploymentReconciler, components *components, insta log.Info("Found identical Service", "all", all, "namespace", found.Namespace, "name", found.Name, "status", found.Status) if instance.Status.ServiceStatus == nil { - instance.Status.ServiceStatus = map[string]machinelearningv1alpha2.ServiceStatus{} + instance.Status.ServiceStatus = map[string]machinelearningv1.ServiceStatus{} } if _, ok := instance.Status.ServiceStatus[found.Name]; !ok { @@ -978,7 +978,7 @@ func createServices(r *SeldonDeploymentReconciler, components *components, insta } // Create Services specified in components. -func createHpas(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1alpha2.SeldonDeployment, log logr.Logger) (bool, error) { +func createHpas(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1.SeldonDeployment, log logr.Logger) (bool, error) { ready := true for _, hpa := range components.hpas { if err := ctrl.SetControllerReference(instance, hpa, r.Scheme); err != nil { @@ -1046,7 +1046,7 @@ func jsonEquals(a, b interface{}) (bool, error) { } // Create Deployments specified in components. -func createDeployments(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1alpha2.SeldonDeployment, log logr.Logger) (bool, error) { +func createDeployments(r *SeldonDeploymentReconciler, components *components, instance *machinelearningv1.SeldonDeployment, log logr.Logger) (bool, error) { ready := true for _, deploy := range components.deployments { @@ -1105,14 +1105,14 @@ func createDeployments(r *SeldonDeploymentReconciler, components *components, in deploymentStatus, present := instance.Status.DeploymentStatus[found.Name] if !present { - deploymentStatus = machinelearningv1alpha2.DeploymentStatus{} + deploymentStatus = machinelearningv1.DeploymentStatus{} } if deploymentStatus.Replicas != found.Status.Replicas || deploymentStatus.AvailableReplicas != found.Status.AvailableReplicas { deploymentStatus.Replicas = found.Status.Replicas deploymentStatus.AvailableReplicas = found.Status.AvailableReplicas if instance.Status.DeploymentStatus == nil { - instance.Status.DeploymentStatus = map[string]machinelearningv1alpha2.DeploymentStatus{} + instance.Status.DeploymentStatus = map[string]machinelearningv1.DeploymentStatus{} } instance.Status.DeploymentStatus[found.Name] = deploymentStatus @@ -1168,7 +1168,7 @@ func createDeployments(r *SeldonDeploymentReconciler, components *components, in if err != nil && errors.IsNotFound(err) { } else { - if _, ok := found.ObjectMeta.Labels[machinelearningv1alpha2.Label_svc_orch]; ok { + if _, ok := found.ObjectMeta.Labels[machinelearningv1.Label_svc_orch]; ok { log.Info("Found existing svc-orch") svcOrchExists = true break @@ -1189,7 +1189,7 @@ func createDeployments(r *SeldonDeploymentReconciler, components *components, in return ready, err } else { if svcOrchExists { - if _, ok := found.ObjectMeta.Labels[machinelearningv1alpha2.Label_svc_orch]; ok { + if _, ok := found.ObjectMeta.Labels[machinelearningv1.Label_svc_orch]; ok { log.Info("Deleting old svc-orch deployment ", "name", k) err := r.Delete(context.TODO(), found, client.PropagationPolicy(metav1.DeletePropagationForeground)) @@ -1261,7 +1261,7 @@ func (r *SeldonDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e log.Info("Reconcile called") // your logic here // Fetch the SeldonDeployment instance - instance := &machinelearningv1alpha2.SeldonDeployment{} + instance := &machinelearningv1.SeldonDeployment{} err := r.Get(ctx, req.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { @@ -1337,7 +1337,7 @@ func (r *SeldonDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e var ( ownerKey = ".metadata.controller" - apiGVStr = machinelearningv1alpha2.GroupVersion.String() + apiGVStr = machinelearningv1.GroupVersion.String() ) func (r *SeldonDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { @@ -1397,14 +1397,14 @@ func (r *SeldonDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { return err } return ctrl.NewControllerManagedBy(mgr). - For(&machinelearningv1alpha2.SeldonDeployment{}). + For(&machinelearningv1.SeldonDeployment{}). Owns(&appsv1.Deployment{}). Owns(&corev1.Service{}). Owns(&istio.VirtualService{}). Complete(r) } else { return ctrl.NewControllerManagedBy(mgr). - For(&machinelearningv1alpha2.SeldonDeployment{}). + For(&machinelearningv1.SeldonDeployment{}). Owns(&appsv1.Deployment{}). Owns(&corev1.Service{}). Complete(r) diff --git a/operator/controllers/seldondeployment_controller_test.go b/operator/controllers/seldondeployment_controller_test.go index 6d06720fd5..daf3d8718b 100644 --- a/operator/controllers/seldondeployment_controller_test.go +++ b/operator/controllers/seldondeployment_controller_test.go @@ -20,7 +20,7 @@ import ( "context" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "io/ioutil" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" @@ -42,22 +42,22 @@ var _ = Describe("Create a Seldon Deployment", func() { By("Creating a resource") It("should create a resource with defaults", func() { Expect(k8sClient).NotTo(BeNil()) - var modelType = machinelearningv1alpha2.MODEL + var modelType = machinelearningv1.MODEL key := types.NamespacedName{ Name: "dep", Namespace: "default", } - instance := &machinelearningv1alpha2.SeldonDeployment{ + instance := &machinelearningv1.SeldonDeployment{ ObjectMeta: metav1.ObjectMeta{ Name: key.Name, Namespace: key.Namespace, }, - Spec: machinelearningv1alpha2.SeldonDeploymentSpec{ + Spec: machinelearningv1.SeldonDeploymentSpec{ Name: "mydep", - Predictors: []machinelearningv1alpha2.PredictorSpec{ + Predictors: []machinelearningv1.PredictorSpec{ { Name: "p1", - ComponentSpecs: []*machinelearningv1alpha2.SeldonPodSpec{ + ComponentSpecs: []*machinelearningv1.SeldonPodSpec{ { Spec: v1.PodSpec{ Containers: []v1.Container{ @@ -69,7 +69,7 @@ var _ = Describe("Create a Seldon Deployment", func() { }, }, }, - Graph: &machinelearningv1alpha2.PredictiveUnit{ + Graph: &machinelearningv1.PredictiveUnit{ Name: "classifier", Type: &modelType, }, @@ -84,7 +84,7 @@ var _ = Describe("Create a Seldon Deployment", func() { Expect(k8sClient.Create(context.Background(), instance)).Should(Succeed()) //time.Sleep(time.Second * 5) - fetched := &machinelearningv1alpha2.SeldonDeployment{} + fetched := &machinelearningv1.SeldonDeployment{} Eventually(func() error { err := k8sClient.Get(context.Background(), key, fetched) return err @@ -92,7 +92,7 @@ var _ = Describe("Create a Seldon Deployment", func() { Expect(fetched.Spec.Name).Should(Equal("mydep")) depKey := types.NamespacedName{ - Name: machinelearningv1alpha2.GetDeploymentName(instance, instance.Spec.Predictors[0], instance.Spec.Predictors[0].ComponentSpecs[0]), + Name: machinelearningv1.GetDeploymentName(instance, instance.Spec.Predictors[0], instance.Spec.Predictors[0].ComponentSpecs[0]), Namespace: "default", } depFetched := &appsv1.Deployment{} diff --git a/operator/controllers/seldondeployment_engine.go b/operator/controllers/seldondeployment_engine.go index 0ae66f4039..8636ca6b4a 100644 --- a/operator/controllers/seldondeployment_engine.go +++ b/operator/controllers/seldondeployment_engine.go @@ -17,7 +17,7 @@ limitations under the License. package controllers import ( - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -32,7 +32,7 @@ var ( EngineContainerName = "seldon-container-engine" ) -func addEngineToDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p *machinelearningv1alpha2.PredictorSpec, engine_http_port int, engine_grpc_port int, pSvcName string, deploy *appsv1.Deployment) error { +func addEngineToDeployment(mlDep *machinelearningv1.SeldonDeployment, p *machinelearningv1.PredictorSpec, engine_http_port int, engine_grpc_port int, pSvcName string, deploy *appsv1.Deployment) error { //check not already present for _, con := range deploy.Spec.Template.Spec.Containers { if strings.Compare(con.Name, EngineContainerName) == 0 { @@ -43,19 +43,19 @@ func addEngineToDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p *m if err != nil { return err } - deploy.Labels[machinelearningv1alpha2.Label_svc_orch] = "true" + deploy.Labels[machinelearningv1.Label_svc_orch] = "true" //downward api used to make pod info available to container volMount := false for _, vol := range engineContainer.VolumeMounts { - if vol.Name == machinelearningv1alpha2.PODINFO_VOLUME_NAME { + if vol.Name == machinelearningv1.PODINFO_VOLUME_NAME { volMount = true } } if !volMount { engineContainer.VolumeMounts = append(engineContainer.VolumeMounts, corev1.VolumeMount{ - Name: machinelearningv1alpha2.PODINFO_VOLUME_NAME, - MountPath: machinelearningv1alpha2.PODINFO_VOLUME_PATH, + Name: machinelearningv1.PODINFO_VOLUME_NAME, + MountPath: machinelearningv1.PODINFO_VOLUME_PATH, }) } @@ -75,13 +75,13 @@ func addEngineToDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p *m deploy.Spec.Template.Annotations["prometheus.io/port"] = strconv.Itoa(engine_http_port) deploy.Spec.Template.Annotations["prometheus.io/scrape"] = "true" - 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 + deploy.ObjectMeta.Labels[machinelearningv1.Label_seldon_app] = pSvcName + deploy.Spec.Selector.MatchLabels[machinelearningv1.Label_seldon_app] = pSvcName + deploy.Spec.Template.ObjectMeta.Labels[machinelearningv1.Label_seldon_app] = pSvcName volFound := false for _, vol := range deploy.Spec.Template.Spec.Volumes { - if vol.Name == machinelearningv1alpha2.PODINFO_VOLUME_NAME { + if vol.Name == machinelearningv1.PODINFO_VOLUME_NAME { volFound = true } } @@ -89,7 +89,7 @@ func addEngineToDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p *m if !volFound { var defaultMode = corev1.DownwardAPIVolumeSourceDefaultMode //Add downwardAPI - deploy.Spec.Template.Spec.Volumes = append(deploy.Spec.Template.Spec.Volumes, corev1.Volume{Name: machinelearningv1alpha2.PODINFO_VOLUME_NAME, VolumeSource: corev1.VolumeSource{ + deploy.Spec.Template.Spec.Volumes = append(deploy.Spec.Template.Spec.Volumes, corev1.Volume{Name: machinelearningv1.PODINFO_VOLUME_NAME, VolumeSource: corev1.VolumeSource{ DownwardAPI: &corev1.DownwardAPIVolumeSource{Items: []corev1.DownwardAPIVolumeFile{ {Path: "annotations", FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.annotations", APIVersion: "v1"}}}, DefaultMode: &defaultMode}}}) } @@ -98,7 +98,7 @@ func addEngineToDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p *m } // Create the Container for the service orchestrator. -func createEngineContainer(mlDep *machinelearningv1alpha2.SeldonDeployment, p *machinelearningv1alpha2.PredictorSpec, engine_http_port, engine_grpc_port int) (*corev1.Container, error) { +func createEngineContainer(mlDep *machinelearningv1.SeldonDeployment, p *machinelearningv1.PredictorSpec, engine_http_port, engine_grpc_port int) (*corev1.Container, error) { // Get engine user var engineUser int64 = -1 if engineUserEnv, ok := os.LookupEnv("ENGINE_CONTAINER_USER"); ok { @@ -119,7 +119,7 @@ func createEngineContainer(mlDep *machinelearningv1alpha2.SeldonDeployment, p *m } //get annotation for java opts or default - javaOpts := getAnnotation(mlDep, machinelearningv1alpha2.ANNOTATION_JAVA_OPTS, "-server -Dcom.sun.management.jmxremote.rmi.port=9090 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=false -Djava.rmi.server.hostname=127.0.0.1") + javaOpts := getAnnotation(mlDep, machinelearningv1.ANNOTATION_JAVA_OPTS, "-server -Dcom.sun.management.jmxremote.rmi.port=9090 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=false -Djava.rmi.server.hostname=127.0.0.1") //Engine resources engineResources := p.SvcOrchSpec.Resources @@ -140,8 +140,8 @@ func createEngineContainer(mlDep *machinelearningv1alpha2.SeldonDeployment, p *m TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { - Name: machinelearningv1alpha2.PODINFO_VOLUME_NAME, - MountPath: machinelearningv1alpha2.PODINFO_VOLUME_PATH, + Name: machinelearningv1.PODINFO_VOLUME_NAME, + MountPath: machinelearningv1.PODINFO_VOLUME_PATH, }, }, Env: []corev1.EnvVar{ @@ -213,12 +213,12 @@ func createEngineContainer(mlDep *machinelearningv1alpha2.SeldonDeployment, p *m } // Create the service orchestrator. -func createEngineDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p *machinelearningv1alpha2.PredictorSpec, seldonId string, engine_http_port, engine_grpc_port int) (*appsv1.Deployment, error) { +func createEngineDeployment(mlDep *machinelearningv1.SeldonDeployment, p *machinelearningv1.PredictorSpec, seldonId string, engine_http_port, engine_grpc_port int) (*appsv1.Deployment, error) { var terminationGracePeriodSecs = int64(20) var defaultMode = corev1.DownwardAPIVolumeSourceDefaultMode - depName := machinelearningv1alpha2.GetServiceOrchestratorName(mlDep, p) + depName := machinelearningv1.GetServiceOrchestratorName(mlDep, p) con, err := createEngineContainer(mlDep, p, engine_http_port, engine_grpc_port) if err != nil { @@ -228,16 +228,16 @@ func createEngineDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p * ObjectMeta: metav1.ObjectMeta{ Name: depName, Namespace: getNamespace(mlDep), - Labels: map[string]string{machinelearningv1alpha2.Label_svc_orch: "true", machinelearningv1alpha2.Label_seldon_app: seldonId, machinelearningv1alpha2.Label_seldon_id: seldonId, "app": depName, "version": "v1", "fluentd": "true"}, + Labels: map[string]string{machinelearningv1.Label_svc_orch: "true", machinelearningv1.Label_seldon_app: seldonId, machinelearningv1.Label_seldon_id: seldonId, "app": depName, "version": "v1", "fluentd": "true"}, Annotations: mlDep.Spec.Annotations, }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{machinelearningv1alpha2.Label_seldon_app: seldonId, machinelearningv1alpha2.Label_seldon_id: seldonId}, + MatchLabels: map[string]string{machinelearningv1.Label_seldon_app: seldonId, machinelearningv1.Label_seldon_id: seldonId}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{machinelearningv1alpha2.Label_seldon_app: seldonId, machinelearningv1alpha2.Label_seldon_id: seldonId, "app": depName}, + Labels: map[string]string{machinelearningv1.Label_seldon_app: seldonId, machinelearningv1.Label_seldon_id: seldonId, "app": depName}, Annotations: map[string]string{ "prometheus.io/path": GetEnv("ENGINE_PROMETHEUS_PATH", "/prometheus"), "prometheus.io/port": strconv.Itoa(engine_http_port), @@ -253,7 +253,7 @@ func createEngineDeployment(mlDep *machinelearningv1alpha2.SeldonDeployment, p * SchedulerName: "default-scheduler", SecurityContext: &corev1.PodSecurityContext{}, Volumes: []corev1.Volume{ - {Name: machinelearningv1alpha2.PODINFO_VOLUME_NAME, VolumeSource: corev1.VolumeSource{DownwardAPI: &corev1.DownwardAPIVolumeSource{Items: []corev1.DownwardAPIVolumeFile{ + {Name: machinelearningv1.PODINFO_VOLUME_NAME, VolumeSource: corev1.VolumeSource{DownwardAPI: &corev1.DownwardAPIVolumeSource{Items: []corev1.DownwardAPIVolumeFile{ {Path: "annotations", FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.annotations", APIVersion: "v1"}}, }, DefaultMode: &defaultMode}}}, }, diff --git a/operator/controllers/seldondeployment_explainers.go b/operator/controllers/seldondeployment_explainers.go index d138a00016..4122c7e637 100644 --- a/operator/controllers/seldondeployment_explainers.go +++ b/operator/controllers/seldondeployment_explainers.go @@ -18,7 +18,7 @@ package controllers import ( "github.com/go-logr/logr" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "github.com/seldonio/seldon-core/operator/utils" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,13 +30,13 @@ import ( "strings" ) -func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alpha2.SeldonDeployment, p *machinelearningv1alpha2.PredictorSpec, c *components, pSvcName string, log logr.Logger) error { +func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1.SeldonDeployment, p *machinelearningv1.PredictorSpec, c *components, pSvcName string, log logr.Logger) error { if p.Explainer.Type != "" { - seldonId := machinelearningv1alpha2.GetSeldonDeploymentName(mlDep) + seldonId := machinelearningv1.GetSeldonDeploymentName(mlDep) - depName := machinelearningv1alpha2.GetExplainerDeploymentName(mlDep.ObjectMeta.Name, p) + depName := machinelearningv1.GetExplainerDeploymentName(mlDep.ObjectMeta.Name, p) explainerContainer := p.Explainer.ContainerSpec @@ -49,7 +49,7 @@ func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alph } if p.Graph.Endpoint == nil { - p.Graph.Endpoint = &machinelearningv1alpha2.Endpoint{Type: machinelearningv1alpha2.REST} + p.Graph.Endpoint = &machinelearningv1.Endpoint{Type: machinelearningv1.REST} } if explainerContainer.Image == "" { @@ -71,7 +71,7 @@ func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alph httpPort = int(portNum) customPort := getPort(portType, explainerContainer.Ports) - if p.Explainer.Endpoint != nil && p.Explainer.Endpoint.Type == machinelearningv1alpha2.GRPC { + if p.Explainer.Endpoint != nil && p.Explainer.Endpoint.Type == machinelearningv1.GRPC { explainerProtocol = "grpc" pSvcEndpoint = c.serviceDetails[pSvcName].GrpcEndpoint } else { @@ -111,7 +111,7 @@ func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alph explainerContainer.Args = append(explainerContainer.Args, string(p.Explainer.Type)) - if p.Explainer.Type == machinelearningv1alpha2.AlibiAnchorsImageExplainer { + if p.Explainer.Type == machinelearningv1.AlibiAnchorsImageExplainer { explainerContainer.Args = append(explainerContainer.Args, "--tf_data_type=float32") } @@ -133,16 +133,16 @@ func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alph // see https://github.com/cliveseldon/kfserving/tree/explainer_update_jul/docs/samples/explanation/income for more // Add Environment Variables - TODO: are these needed - if !utils.HasEnvVar(explainerContainer.Env, machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_SERVICE_PORT) { + if !utils.HasEnvVar(explainerContainer.Env, machinelearningv1.ENV_PREDICTIVE_UNIT_SERVICE_PORT) { explainerContainer.Env = append(explainerContainer.Env, []corev1.EnvVar{ - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_SERVICE_PORT, Value: strconv.Itoa(int(portNum))}, - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_ID, Value: explainerContainer.Name}, - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTOR_ID, Value: p.Name}, - corev1.EnvVar{Name: machinelearningv1alpha2.ENV_SELDON_DEPLOYMENT_ID, Value: mlDep.ObjectMeta.Name}, + corev1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_SERVICE_PORT, Value: strconv.Itoa(int(portNum))}, + corev1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_ID, Value: explainerContainer.Name}, + corev1.EnvVar{Name: machinelearningv1.ENV_PREDICTOR_ID, Value: p.Name}, + corev1.EnvVar{Name: machinelearningv1.ENV_SELDON_DEPLOYMENT_ID, Value: mlDep.ObjectMeta.Name}, }...) } - seldonPodSpec := machinelearningv1alpha2.SeldonPodSpec{Spec: corev1.PodSpec{ + seldonPodSpec := machinelearningv1.SeldonPodSpec{Spec: corev1.PodSpec{ Containers: []corev1.Container{explainerContainer}, }} @@ -157,10 +157,10 @@ func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alph } // for explainer use same service name as its Deployment - eSvcName := machinelearningv1alpha2.GetExplainerDeploymentName(mlDep.ObjectMeta.Name, p) + eSvcName := machinelearningv1.GetExplainerDeploymentName(mlDep.ObjectMeta.Name, p) - deploy.ObjectMeta.Labels[machinelearningv1alpha2.Label_seldon_app] = eSvcName - deploy.Spec.Template.ObjectMeta.Labels[machinelearningv1alpha2.Label_seldon_app] = eSvcName + deploy.ObjectMeta.Labels[machinelearningv1.Label_seldon_app] = eSvcName + deploy.Spec.Template.ObjectMeta.Labels[machinelearningv1.Label_seldon_app] = eSvcName c.deployments = append(c.deployments, deploy) @@ -170,10 +170,10 @@ func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alph return err } c.services = append(c.services, eSvc) - c.serviceDetails[eSvcName] = &machinelearningv1alpha2.ServiceStatus{ + c.serviceDetails[eSvcName] = &machinelearningv1.ServiceStatus{ SvcName: eSvcName, HttpEndpoint: eSvcName + "." + eSvc.Namespace + ":" + strconv.Itoa(httpPort), - ExplainerFor: machinelearningv1alpha2.GetPredictorKey(mlDep, p), + ExplainerFor: machinelearningv1.GetPredictorKey(mlDep, p), } if grpcPort > 0 { c.serviceDetails[eSvcName].GrpcEndpoint = eSvcName + "." + eSvc.Namespace + ":" + strconv.Itoa(grpcPort) @@ -190,8 +190,8 @@ func createExplainer(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alph // Create istio virtual service and destination rule for explainer. // Explainers need one each with no traffic-splitting -func createExplainerIstioResources(pSvcName string, p *machinelearningv1alpha2.PredictorSpec, - mlDep *machinelearningv1alpha2.SeldonDeployment, +func createExplainerIstioResources(pSvcName string, p *machinelearningv1.PredictorSpec, + mlDep *machinelearningv1.SeldonDeployment, seldonId string, namespace string, engine_http_port int, diff --git a/operator/controllers/seldondeployment_prepackaged_servers.go b/operator/controllers/seldondeployment_prepackaged_servers.go index 43193e1a62..8eb131f811 100644 --- a/operator/controllers/seldondeployment_prepackaged_servers.go +++ b/operator/controllers/seldondeployment_prepackaged_servers.go @@ -18,7 +18,7 @@ package controllers import ( "encoding/json" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "github.com/seldonio/seldon-core/operator/constants" "github.com/seldonio/seldon-core/operator/utils" appsv1 "k8s.io/api/apps/v1" @@ -26,15 +26,15 @@ import ( "strings" ) -func addTFServerContainer(r *SeldonDeploymentReconciler, pu *machinelearningv1alpha2.PredictiveUnit, p *machinelearningv1alpha2.PredictorSpec, deploy *appsv1.Deployment, serverConfig machinelearningv1alpha2.PredictorServerConfig) error { +func addTFServerContainer(r *SeldonDeploymentReconciler, pu *machinelearningv1.PredictiveUnit, p *machinelearningv1.PredictorSpec, deploy *appsv1.Deployment, serverConfig machinelearningv1.PredictorServerConfig) error { if len(*pu.Implementation) > 0 && (serverConfig.Tensorflow || serverConfig.TensorflowImage != "") { - ty := machinelearningv1alpha2.MODEL + ty := machinelearningv1.MODEL pu.Type = &ty if pu.Endpoint == nil { - pu.Endpoint = &machinelearningv1alpha2.Endpoint{Type: machinelearningv1alpha2.REST} + pu.Endpoint = &machinelearningv1.Endpoint{Type: machinelearningv1.REST} } c := utils.GetContainerForDeployment(deploy, pu.Name) @@ -44,15 +44,15 @@ func addTFServerContainer(r *SeldonDeploymentReconciler, pu *machinelearningv1al Name: pu.Name, VolumeMounts: []v1.VolumeMount{ { - Name: machinelearningv1alpha2.PODINFO_VOLUME_NAME, - MountPath: machinelearningv1alpha2.PODINFO_VOLUME_PATH, + Name: machinelearningv1.PODINFO_VOLUME_NAME, + MountPath: machinelearningv1.PODINFO_VOLUME_PATH, }, }, } } //Add missing fields - machinelearningv1alpha2.SetImageNameForPrepackContainer(pu, c) + machinelearningv1.SetImageNameForPrepackContainer(pu, c) SetUriParamsForTFServingProxyContainer(pu, c) // Add container to deployment @@ -67,7 +67,7 @@ func addTFServerContainer(r *SeldonDeploymentReconciler, pu *machinelearningv1al tfServingContainer := utils.GetContainerForDeployment(deploy, constants.TFServingContainerName) existing = tfServingContainer != nil if !existing { - ServerConfig := machinelearningv1alpha2.GetPrepackServerConfig(string(*pu.Implementation)) + ServerConfig := machinelearningv1.GetPrepackServerConfig(string(*pu.Implementation)) tfImage := "tensorflow/serving:latest" @@ -110,15 +110,15 @@ func addTFServerContainer(r *SeldonDeploymentReconciler, pu *machinelearningv1al return nil } -func addModelDefaultServers(r *SeldonDeploymentReconciler, pu *machinelearningv1alpha2.PredictiveUnit, p *machinelearningv1alpha2.PredictorSpec, deploy *appsv1.Deployment, serverConfig machinelearningv1alpha2.PredictorServerConfig) error { +func addModelDefaultServers(r *SeldonDeploymentReconciler, pu *machinelearningv1.PredictiveUnit, p *machinelearningv1.PredictorSpec, deploy *appsv1.Deployment, serverConfig machinelearningv1.PredictorServerConfig) error { if len(*pu.Implementation) > 0 && !serverConfig.Tensorflow && serverConfig.TensorflowImage == "" { - ty := machinelearningv1alpha2.MODEL + ty := machinelearningv1.MODEL pu.Type = &ty if pu.Endpoint == nil { - pu.Endpoint = &machinelearningv1alpha2.Endpoint{Type: machinelearningv1alpha2.REST} + pu.Endpoint = &machinelearningv1.Endpoint{Type: machinelearningv1.REST} } c := utils.GetContainerForDeployment(deploy, pu.Name) existing := c != nil @@ -127,18 +127,18 @@ func addModelDefaultServers(r *SeldonDeploymentReconciler, pu *machinelearningv1 Name: pu.Name, VolumeMounts: []v1.VolumeMount{ { - Name: machinelearningv1alpha2.PODINFO_VOLUME_NAME, - MountPath: machinelearningv1alpha2.PODINFO_VOLUME_PATH, + Name: machinelearningv1.PODINFO_VOLUME_NAME, + MountPath: machinelearningv1.PODINFO_VOLUME_PATH, }, }, } } - machinelearningv1alpha2.SetImageNameForPrepackContainer(pu, c) + machinelearningv1.SetImageNameForPrepackContainer(pu, c) // Add parameters envvar - point at mount path because initContainer will download params := pu.Parameters - uriParam := machinelearningv1alpha2.Parameter{ + uriParam := machinelearningv1.Parameter{ Name: "model_uri", Type: "STRING", Value: DefaultModelLocalMountPath, @@ -150,10 +150,10 @@ func addModelDefaultServers(r *SeldonDeploymentReconciler, pu *machinelearningv1 } if len(params) > 0 { - if !utils.HasEnvVar(c.Env, machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS) { - c.Env = append(c.Env, v1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: string(paramStr)}) + if !utils.HasEnvVar(c.Env, machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS) { + c.Env = append(c.Env, v1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: string(paramStr)}) } else { - c.Env = utils.SetEnvVar(c.Env, v1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: string(paramStr)}) + c.Env = utils.SetEnvVar(c.Env, v1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: string(paramStr)}) } } @@ -175,7 +175,7 @@ func addModelDefaultServers(r *SeldonDeploymentReconciler, pu *machinelearningv1 return nil } -func SetUriParamsForTFServingProxyContainer(pu *machinelearningv1alpha2.PredictiveUnit, c *v1.Container) { +func SetUriParamsForTFServingProxyContainer(pu *machinelearningv1.PredictiveUnit, c *v1.Container) { parameters := pu.Parameters @@ -189,16 +189,16 @@ func SetUriParamsForTFServingProxyContainer(pu *machinelearningv1alpha2.Predicti } } if !hasUriParams { - var uriParam machinelearningv1alpha2.Parameter + var uriParam machinelearningv1.Parameter - if pu.Endpoint.Type == machinelearningv1alpha2.REST { - uriParam = machinelearningv1alpha2.Parameter{ + if pu.Endpoint.Type == machinelearningv1.REST { + uriParam = machinelearningv1.Parameter{ Name: "rest_endpoint", Type: "STRING", Value: "http://0.0.0.0:2001", } } else { - uriParam = machinelearningv1alpha2.Parameter{ + uriParam = machinelearningv1.Parameter{ Name: "grpc_endpoint", Type: "STRING", Value: "0.0.0.0:2000", @@ -208,7 +208,7 @@ func SetUriParamsForTFServingProxyContainer(pu *machinelearningv1alpha2.Predicti parameters = append(pu.Parameters, uriParam) - modelNameParam := machinelearningv1alpha2.Parameter{ + modelNameParam := machinelearningv1.Parameter{ Name: "model_name", Type: "STRING", Value: pu.Name, @@ -219,21 +219,21 @@ func SetUriParamsForTFServingProxyContainer(pu *machinelearningv1alpha2.Predicti } if len(parameters) > 0 { - if !utils.HasEnvVar(c.Env, machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS) { - c.Env = append(c.Env, v1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: utils.GetPredictiveUnitAsJson(parameters)}) + if !utils.HasEnvVar(c.Env, machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS) { + c.Env = append(c.Env, v1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: utils.GetPredictiveUnitAsJson(parameters)}) } else { - c.Env = utils.SetEnvVar(c.Env, v1.EnvVar{Name: machinelearningv1alpha2.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: utils.GetPredictiveUnitAsJson(parameters)}) + c.Env = utils.SetEnvVar(c.Env, v1.EnvVar{Name: machinelearningv1.ENV_PREDICTIVE_UNIT_PARAMETERS, Value: utils.GetPredictiveUnitAsJson(parameters)}) } } } -func createStandaloneModelServers(r *SeldonDeploymentReconciler, mlDep *machinelearningv1alpha2.SeldonDeployment, p *machinelearningv1alpha2.PredictorSpec, c *components, pu *machinelearningv1alpha2.PredictiveUnit) error { +func createStandaloneModelServers(r *SeldonDeploymentReconciler, mlDep *machinelearningv1.SeldonDeployment, p *machinelearningv1.PredictorSpec, c *components, pu *machinelearningv1.PredictiveUnit) error { // some predictors have no podSpec so this could be nil sPodSpec := utils.GetSeldonPodSpecForPredictiveUnit(p, pu.Name) - depName := machinelearningv1alpha2.GetDeploymentName(mlDep, *p, sPodSpec) + depName := machinelearningv1.GetDeploymentName(mlDep, *p, sPodSpec) var deploy *appsv1.Deployment existing := false @@ -248,13 +248,13 @@ func createStandaloneModelServers(r *SeldonDeploymentReconciler, mlDep *machinel // might not be a Deployment yet - if so we have to create one if deploy == nil { - seldonId := machinelearningv1alpha2.GetSeldonDeploymentName(mlDep) + seldonId := machinelearningv1.GetSeldonDeploymentName(mlDep) deploy = createDeploymentWithoutEngine(depName, seldonId, sPodSpec, p, mlDep) } - if machinelearningv1alpha2.IsPrepack(pu) { + if machinelearningv1.IsPrepack(pu) { - ServerConfig := machinelearningv1alpha2.GetPrepackServerConfig(string(*pu.Implementation)) + ServerConfig := machinelearningv1.GetPrepackServerConfig(string(*pu.Implementation)) if err := addModelDefaultServers(r, pu, p, deploy, ServerConfig); err != nil { return err diff --git a/operator/controllers/seldondeployment_prepackaged_servers_test.go b/operator/controllers/seldondeployment_prepackaged_servers_test.go index 1d088407c7..6385e4b18f 100644 --- a/operator/controllers/seldondeployment_prepackaged_servers_test.go +++ b/operator/controllers/seldondeployment_prepackaged_servers_test.go @@ -4,7 +4,7 @@ import ( "context" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "github.com/seldonio/seldon-core/operator/utils" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -19,27 +19,27 @@ var _ = Describe("Create a prepacked sklearn server", func() { By("Creating a resource") It("should create a resource with defaults", func() { Expect(k8sClient).NotTo(BeNil()) - var modelType = machinelearningv1alpha2.MODEL - var impl = machinelearningv1alpha2.PredictiveUnitImplementation("SKLEARN_SERVER") + var modelType = machinelearningv1.MODEL + var impl = machinelearningv1.PredictiveUnitImplementation("SKLEARN_SERVER") key := types.NamespacedName{ Name: "prepack", Namespace: "default", } - instance := &machinelearningv1alpha2.SeldonDeployment{ + instance := &machinelearningv1.SeldonDeployment{ ObjectMeta: metav1.ObjectMeta{ Name: key.Name, Namespace: key.Namespace, }, - Spec: machinelearningv1alpha2.SeldonDeploymentSpec{ + Spec: machinelearningv1.SeldonDeploymentSpec{ Name: "pp", - Predictors: []machinelearningv1alpha2.PredictorSpec{ + Predictors: []machinelearningv1.PredictorSpec{ { Name: "p1", - Graph: &machinelearningv1alpha2.PredictiveUnit{ + Graph: &machinelearningv1.PredictiveUnit{ Name: "classifier", Type: &modelType, Implementation: &impl, - Endpoint: &machinelearningv1alpha2.Endpoint{Type: machinelearningv1alpha2.REST}, + Endpoint: &machinelearningv1.Endpoint{Type: machinelearningv1.REST}, }, }, }, @@ -60,7 +60,7 @@ var _ = Describe("Create a prepacked sklearn server", func() { Expect(k8sClient.Create(context.Background(), instance)).Should(Succeed()) //time.Sleep(time.Second * 5) - fetched := &machinelearningv1alpha2.SeldonDeployment{} + fetched := &machinelearningv1.SeldonDeployment{} Eventually(func() error { err := k8sClient.Get(context.Background(), key, fetched) return err @@ -68,7 +68,7 @@ var _ = Describe("Create a prepacked sklearn server", func() { 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) + depName := machinelearningv1.GetDeploymentName(instance, instance.Spec.Predictors[0], sPodSpec) depKey := types.NamespacedName{ Name: depName, Namespace: "default", diff --git a/operator/controllers/suite_test.go b/operator/controllers/suite_test.go index a902d38c3b..729c219233 100644 --- a/operator/controllers/suite_test.go +++ b/operator/controllers/suite_test.go @@ -21,7 +21,7 @@ import ( "fmt" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -110,7 +110,7 @@ var configs = map[string]string{ // Create configmap var configMap = &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: machinelearningv1alpha2.ControllerConfigMapName, + Name: machinelearningv1.ControllerConfigMapName, Namespace: "seldon-system", }, Data: configs, @@ -152,7 +152,7 @@ var _ = BeforeSuite(func(done Done) { err = corev1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) - err = machinelearningv1alpha2.AddToScheme(scheme) + err = machinelearningv1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = istio.AddToScheme(scheme) @@ -180,7 +180,7 @@ var _ = BeforeSuite(func(done Done) { Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred()) // defer k8sClient.Delete(context.TODO(), configMap) - machinelearningv1alpha2.C = k8sClient + machinelearningv1.C = k8sClient fmt.Println("test k8s client") fmt.Printf("%+v\n", k8sClient) @@ -189,7 +189,7 @@ var _ = BeforeSuite(func(done Done) { defer GinkgoRecover() //can't call webhook as leads to https://github.com/kubernetes-sigs/controller-runtime/issues/491 - //err = (&machinelearningv1alpha2.SeldonDeployment{}).SetupWebhookWithManager(k8sManager) + //err = (&machinelearningv1.SeldonDeployment{}).SetupWebhookWithManager(k8sManager) //Expect(err).ToNot(HaveOccurred()) err = k8sManager.Start(ctrl.SetupSignalHandler()) Expect(err).ToNot(HaveOccurred()) diff --git a/operator/main.go b/operator/main.go index d995bc8e15..ea7f015b5b 100644 --- a/operator/main.go +++ b/operator/main.go @@ -20,7 +20,7 @@ import ( "flag" "os" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" "github.com/seldonio/seldon-core/operator/controllers" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -43,7 +43,7 @@ func init() { _ = appsv1.AddToScheme(scheme) _ = corev1.AddToScheme(scheme) - _ = machinelearningv1alpha2.AddToScheme(scheme) + _ = machinelearningv1.AddToScheme(scheme) if controllers.GetEnv(controllers.ENV_ISTIO_ENABLED, "false") == "true" { istio.AddToScheme(scheme) } @@ -86,7 +86,7 @@ func main() { os.Exit(1) } - if err = (&machinelearningv1alpha2.SeldonDeployment{}).SetupWebhookWithManager(mgr); err != nil { + if err = (&machinelearningv1.SeldonDeployment{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "SeldonDeployment") os.Exit(1) } diff --git a/operator/utils/utils.go b/operator/utils/utils.go index 3e905bad4a..cd7755eb4f 100644 --- a/operator/utils/utils.go +++ b/operator/utils/utils.go @@ -2,13 +2,13 @@ package utils import ( "encoding/json" - machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "strings" ) -func GetPredictiveUnitAsJson(params []machinelearningv1alpha2.Parameter) string { +func GetPredictiveUnitAsJson(params []machinelearningv1.Parameter) string { str, err := json.Marshal(params) if err != nil { return "" @@ -17,7 +17,7 @@ func GetPredictiveUnitAsJson(params []machinelearningv1alpha2.Parameter) string } } -func GetSeldonPodSpecForPredictiveUnit(p *machinelearningv1alpha2.PredictorSpec, name string) *machinelearningv1alpha2.SeldonPodSpec { +func GetSeldonPodSpecForPredictiveUnit(p *machinelearningv1.PredictorSpec, name string) *machinelearningv1.SeldonPodSpec { for j := 0; j < len(p.ComponentSpecs); j++ { cSpec := p.ComponentSpecs[j] for k := 0; k < len(cSpec.Spec.Containers); k++ { From 53c2a8c3fe4d595346bd2860c40ec09c395dbd54 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Tue, 10 Dec 2019 14:40:40 +0000 Subject: [PATCH 03/12] Set v1 as storagevesion --- operator/api/v1/seldondeployment_types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/operator/api/v1/seldondeployment_types.go b/operator/api/v1/seldondeployment_types.go index d679cee2e3..03bd220ca6 100644 --- a/operator/api/v1/seldondeployment_types.go +++ b/operator/api/v1/seldondeployment_types.go @@ -363,6 +363,7 @@ type SeldonDeploymentStatus struct { // +genclient // +genclient:noStatus // +kubebuilder:object:root=true +// +kubebuilder:storageversion // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // SeldonDeployment is the Schema for the seldondeployments API From ae867db470a318d887121e835e8a8d9cc2abf03b Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Tue, 10 Dec 2019 15:28:25 +0000 Subject: [PATCH 04/12] Add v1alpha3 package --- operator/api/v1alpha3/groupversion_info.go | 36 ++ .../api/v1alpha3/seldondeployment_types.go | 391 +++++++++++++ .../api/v1alpha3/seldondeployment_webhook.go | 537 ++++++++++++++++++ .../api/v1alpha3/zz_generated.deepcopy.go | 443 +++++++++++++++ 4 files changed, 1407 insertions(+) create mode 100644 operator/api/v1alpha3/groupversion_info.go create mode 100644 operator/api/v1alpha3/seldondeployment_types.go create mode 100644 operator/api/v1alpha3/seldondeployment_webhook.go create mode 100644 operator/api/v1alpha3/zz_generated.deepcopy.go diff --git a/operator/api/v1alpha3/groupversion_info.go b/operator/api/v1alpha3/groupversion_info.go new file mode 100644 index 0000000000..a420d0ed02 --- /dev/null +++ b/operator/api/v1alpha3/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +// Package v1alpha3 contains API Schema definitions for the machinelearning v1alpha3 API group +// +kubebuilder:object:generate=true +// +groupName=machinelearning.seldon.io +package v1alpha3 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "machinelearning.seldon.io", Version: "v1alpha3"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/operator/api/v1alpha3/seldondeployment_types.go b/operator/api/v1alpha3/seldondeployment_types.go new file mode 100644 index 0000000000..15c64bf104 --- /dev/null +++ b/operator/api/v1alpha3/seldondeployment_types.go @@ -0,0 +1,391 @@ +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +package v1alpha3 + +import ( + "crypto/md5" + "encoding/hex" + "regexp" + "strings" + + autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + Label_seldon_id = "seldon-deployment-id" + Label_seldon_app = "seldon-app" + Label_svc_orch = "seldon-deployment-contains-svcorch" + + PODINFO_VOLUME_NAME = "podinfo" + PODINFO_VOLUME_PATH = "/etc/podinfo" + + ENV_PREDICTIVE_UNIT_SERVICE_PORT = "PREDICTIVE_UNIT_SERVICE_PORT" + ENV_PREDICTIVE_UNIT_PARAMETERS = "PREDICTIVE_UNIT_PARAMETERS" + ENV_PREDICTIVE_UNIT_ID = "PREDICTIVE_UNIT_ID" + ENV_PREDICTOR_ID = "PREDICTOR_ID" + ENV_SELDON_DEPLOYMENT_ID = "SELDON_DEPLOYMENT_ID" + + 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" +) + +func hash(text string) string { + hasher := md5.New() + hasher.Write([]byte(text)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func containerHash(podSpec *SeldonPodSpec) string { + s := []string{} + for i := 0; i < len(podSpec.Spec.Containers); i++ { + c := podSpec.Spec.Containers[i] + s = append(s, c.Name) + s = append(s, c.Image) + } + key := strings.Join(s, ":") + ";" + return hash(key)[:7] +} + +func createPredictorHash(p *PredictorSpec) string { + s := []string{} + for i := 0; i < len(p.ComponentSpecs); i++ { + s = append(s, containerHash(p.ComponentSpecs[i])) + } + key := strings.Join(s, ",") + "," + return hash(key)[:7] +} + +func GetSeldonDeploymentName(mlDep *SeldonDeployment) string { + name := mlDep.Spec.Name + "-" + mlDep.ObjectMeta.Name + if len(name) > 63 { + return "seldon-" + hash(name) + } else { + return name + } +} + +func GetExplainerDeploymentName(sdepName string, predictorSpec *PredictorSpec) string { + name := sdepName + "-" + predictorSpec.Name + "-explainer" + if len(name) > 63 { + return "seldon-" + hash(name) + } else { + return name + } +} + +func GetDeploymentName(mlDep *SeldonDeployment, predictorSpec PredictorSpec, podSpec *SeldonPodSpec) string { + if podSpec != nil && len(podSpec.Metadata.Name) != 0 { + return podSpec.Metadata.Name + } else { + name := mlDep.Spec.Name + "-" + predictorSpec.Name + if podSpec != nil { + name = name + "-" + containerHash(podSpec) + } + if len(name) > 63 { + return "seldon-" + hash(name) + } else { + return name + } + } +} + +func GetServiceOrchestratorName(mlDep *SeldonDeployment, p *PredictorSpec) string { + svcOrchName := mlDep.Spec.Name + "-" + p.Name + "-svc-orch" + "-" + createPredictorHash(p) + if len(svcOrchName) > 63 { + return "seldon-" + hash(svcOrchName) + } else { + return svcOrchName + } +} + +func GetPredictorKey(mlDep *SeldonDeployment, p *PredictorSpec) string { + if annotation, hasAnnotation := p.Annotations[ANNOTATION_CUSTOM_SVC_NAME]; hasAnnotation { + return annotation + } else { + return getPredictorKeyAutoGenerated(mlDep, p) + } +} + +func getPredictorKeyAutoGenerated(mlDep *SeldonDeployment, p *PredictorSpec) string { + pName := mlDep.Name + "-" + mlDep.Spec.Name + "-" + p.Name + if len(pName) > 63 { + return "seldon-" + hash(pName) + } else { + return pName + } +} + +func GetPredictorServiceNameKey(c *v1.Container) string { + return Label_seldon_app + "-" + c.Name +} + +func GetPredictiveUnit(pu *PredictiveUnit, name string) *PredictiveUnit { + if name == pu.Name { + return pu + } else { + for i := 0; i < len(pu.Children); i++ { + found := GetPredictiveUnit(&pu.Children[i], name) + if found != nil { + return found + } + } + return nil + } +} + +// if engine is not separated then this tells us which pu it should go on, as the mutating webhook handler has set host as localhost on the pu +func GetEnginePredictiveUnit(pu *PredictiveUnit) *PredictiveUnit { + if pu.Endpoint != nil && pu.Endpoint.ServiceHost == "localhost" { + return pu + } else { + for i := 0; i < len(pu.Children); i++ { + found := GetEnginePredictiveUnit(&pu.Children[i]) + if found != nil { + return found + } + } + return nil + } +} + +func GetPredictiveUnitList(p *PredictiveUnit) (list []*PredictiveUnit) { + list = append(list, p) + + for i := 0; i < len(p.Children); i++ { + pu := &p.Children[i] + list = append(list, GetPredictiveUnitList(pu)...) + } + return list +} + +func cleanContainerName(name string) string { + var re = regexp.MustCompile("[^-a-z0-9]") + return re.ReplaceAllString(strings.ToLower(name), "-") +} + +func GetContainerServiceName(mlDep *SeldonDeployment, predictorSpec PredictorSpec, c *v1.Container) string { + containerImageName := cleanContainerName(c.Image) + svcName := mlDep.Spec.Name + "-" + predictorSpec.Name + "-" + c.Name + if containerImageName != "" { + svcName = svcName + "-" + containerImageName + } + if len(svcName) > 63 { + svcName = "seldon" + "-" + containerImageName + "-" + hash(svcName) + if len(svcName) > 63 { + return "seldon-" + hash(svcName) + } else { + return svcName + } + } else { + return svcName + } +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// SeldonDeploymentSpec defines the desired state of SeldonDeployment +type SeldonDeploymentSpec struct { + Name string `json:"name,omitempty" protobuf:"string,1,opt,name=name"` + Predictors []PredictorSpec `json:"predictors" protobuf:"bytes,2,opt,name=name"` + OauthKey string `json:"oauth_key,omitempty" protobuf:"string,3,opt,name=oauth_key"` + OauthSecret string `json:"oauth_secret,omitempty" protobuf:"string,4,opt,name=oauth_secret"` + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,5,opt,name=annotations"` +} + +type PredictorSpec struct { + Name string `json:"name" protobuf:"string,1,opt,name=name"` + Graph *PredictiveUnit `json:"graph" protobuf:"bytes,2,opt,name=predictiveUnit"` + ComponentSpecs []*SeldonPodSpec `json:"componentSpecs,omitempty" protobuf:"bytes,3,opt,name=componentSpecs"` + Replicas int32 `json:"replicas,omitempty" protobuf:"string,4,opt,name=replicas"` + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,5,opt,name=annotations"` + EngineResources v1.ResourceRequirements `json:"engineResources,omitempty" protobuf:"bytes,6,opt,name=engineResources"` + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,7,opt,name=labels"` + SvcOrchSpec SvcOrchSpec `json:"svcOrchSpec,omitempty" protobuf:"bytes,8,opt,name=svcOrchSpec"` + Traffic int32 `json:"traffic,omitempty" protobuf:"bytes,9,opt,name=traffic"` + Explainer Explainer `json:"explainer,omitempty" protobuf:"bytes,10,opt,name=explainer"` + Shadow bool `json:"shadow,omitempty" protobuf:"bytes,11,opt,name=shadow"` +} + +type SvcOrchSpec struct { + Resources *v1.ResourceRequirements `json:"resources,omitempty" protobuf:"bytes,1,opt,name=resources"` + Env []*v1.EnvVar `json:"env,omitempty" protobuf:"bytes,2,opt,name=env"` +} + +type AlibiExplainerType string + +const ( + AlibiAnchorsTabularExplainer AlibiExplainerType = "AnchorTabular" + AlibiAnchorsImageExplainer AlibiExplainerType = "AnchorImages" + AlibiAnchorsTextExplainer AlibiExplainerType = "AnchorText" + AlibiCounterfactualsExplainer AlibiExplainerType = "Counterfactuals" + AlibiContrastiveExplainer AlibiExplainerType = "Contrastive" +) + +type Explainer struct { + Type AlibiExplainerType `json:"type,omitempty" protobuf:"string,1,opt,name=type"` + ModelUri string `json:"modelUri,omitempty" protobuf:"string,2,opt,name=modelUri"` + ServiceAccountName string `json:"serviceAccountName,omitempty" protobuf:"string,3,opt,name=serviceAccountName"` + ContainerSpec v1.Container `json:"containerSpec,omitempty" protobuf:"bytes,4,opt,name=containerSpec"` + Config map[string]string `json:"config,omitempty" protobuf:"bytes,5,opt,name=config"` + Endpoint *Endpoint `json:"endpoint,omitempty" protobuf:"bytes,6,opt,name=endpoint"` + EnvSecretRefName string `json:"envSecretRefName,omitempty" protobuf:"bytes,7,opt,name=envSecretRefName"` +} + +type SeldonPodSpec struct { + Metadata metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + Spec v1.PodSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + HpaSpec *SeldonHpaSpec `json:"hpaSpec,omitempty" protobuf:"bytes,3,opt,name=hpaSpec"` +} + +type SeldonHpaSpec struct { + MinReplicas *int32 `json:"minReplicas,omitempty" protobuf:"int,1,opt,name=minReplicas"` + MaxReplicas int32 `json:"maxReplicas" protobuf:"int,2,opt,name=maxReplicas"` + Metrics []autoscalingv2beta2.MetricSpec `json:"metrics,omitempty" protobuf:"bytes,3,opt,name=metrics"` +} + +type PredictiveUnitType string + +const ( + UNKNOWN_TYPE PredictiveUnitType = "UNKNOWN_TYPE" + ROUTER PredictiveUnitType = "ROUTER" + COMBINER PredictiveUnitType = "COMBINER" + MODEL PredictiveUnitType = "MODEL" + TRANSFORMER PredictiveUnitType = "TRANSFORMER" + OUTPUT_TRANSFORMER PredictiveUnitType = "OUTPUT_TRANSFORMER" +) + +type PredictiveUnitImplementation string + +const ( + UNKNOWN_IMPLEMENTATION PredictiveUnitImplementation = "UNKNOWN_IMPLEMENTATION" + SIMPLE_MODEL PredictiveUnitImplementation = "SIMPLE_MODEL" + SIMPLE_ROUTER PredictiveUnitImplementation = "SIMPLE_ROUTER" + RANDOM_ABTEST PredictiveUnitImplementation = "RANDOM_ABTEST" + AVERAGE_COMBINER PredictiveUnitImplementation = "AVERAGE_COMBINER" +) + +type PredictiveUnitMethod string + +const ( + TRANSFORM_INPUT PredictiveUnitMethod = "TRANSFORM_INPUT" + TRANSFORM_OUTPUT PredictiveUnitMethod = "TRANSFORM_OUTPUT" + ROUTE PredictiveUnitMethod = "ROUTE" + AGGREGATE PredictiveUnitMethod = "AGGREGATE" + SEND_FEEDBACK PredictiveUnitMethod = "SEND_FEEDBACK" +) + +type EndpointType string + +const ( + REST EndpointType = "REST" + GRPC EndpointType = "GRPC" +) + +type Endpoint struct { + ServiceHost string `json:"service_host,omitempty" protobuf:"string,1,opt,name=service_host"` + ServicePort int32 `json:"service_port,omitempty" protobuf:"int32,2,opt,name=service_port"` + Type EndpointType `json:"type,omitempty" protobuf:"int,3,opt,name=type"` +} + +type ParmeterType string + +const ( + INT ParmeterType = "INT" + FLOAT ParmeterType = "FLOAT" + DOUBLE ParmeterType = "DOUBLE" + STRING ParmeterType = "STRING" + BOOL ParmeterType = "BOOL" +) + +type Parameter struct { + Name string `json:"name" protobuf:"string,1,opt,name=name"` + Value string `json:"value" protobuf:"string,2,opt,name=value"` + Type ParmeterType `json:"type" protobuf:"int,3,opt,name=type"` +} + +type PredictiveUnit struct { + Name string `json:"name" protobuf:"string,1,opt,name=name"` + Children []PredictiveUnit `json:"children,omitempty" protobuf:"bytes,2,opt,name=children"` + Type *PredictiveUnitType `json:"type,omitempty" protobuf:"int,3,opt,name=type"` + Implementation *PredictiveUnitImplementation `json:"implementation,omitempty" protobuf:"int,4,opt,name=implementation"` + Methods *[]PredictiveUnitMethod `json:"methods,omitempty" protobuf:"int,5,opt,name=methods"` + Endpoint *Endpoint `json:"endpoint,omitempty" protobuf:"bytes,6,opt,name=endpoint"` + Parameters []Parameter `json:"parameters,omitempty" protobuf:"bytes,7,opt,name=parameters"` + ModelURI string `json:"modelUri,omitempty" protobuf:"bytes,8,opt,name=modelUri"` + ServiceAccountName string `json:"serviceAccountName,omitempty" protobuf:"bytes,9,opt,name=serviceAccountName"` + EnvSecretRefName string `json:"envSecretRefName,omitempty" protobuf:"bytes,10,opt,name=envSecretRefName"` +} + +type DeploymentStatus struct { + Name string `json:"name,omitempty" protobuf:"string,1,opt,name=name"` + Status string `json:"status,omitempty" protobuf:"string,2,opt,name=status"` + Description string `json:"description,omitempty" protobuf:"string,3,opt,name=description"` + Replicas int32 `json:"replicas,omitempty" protobuf:"string,4,opt,name=replicas"` + AvailableReplicas int32 `json:"availableReplicas,omitempty" protobuf:"string,5,opt,name=availableRelicas"` + ExplainerFor string `json:"explainerFor,omitempty" protobuf:"string,6,opt,name=explainerFor"` +} + +type ServiceStatus struct { + SvcName string `json:"svcName,omitempty" protobuf:"string,1,opt,name=svcName"` + HttpEndpoint string `json:"httpEndpoint,omitempty" protobuf:"string,2,opt,name=httpEndpoint"` + GrpcEndpoint string `json:"grpcEndpoint,omitempty" protobuf:"string,3,opt,name=grpcEndpoint"` + ExplainerFor string `json:"explainerFor,omitempty" protobuf:"string,4,opt,name=explainerFor"` +} + +// SeldonDeploymentStatus defines the observed state of SeldonDeployment +type SeldonDeploymentStatus struct { + State string `json:"state,omitempty" protobuf:"string,1,opt,name=state"` + Description string `json:"description,omitempty" protobuf:"string,2,opt,name=description"` + DeploymentStatus map[string]DeploymentStatus `json:"deploymentStatus,omitempty" protobuf:"bytes,3,opt,name=deploymentStatus"` + ServiceStatus map[string]ServiceStatus `json:"serviceStatus,omitempty" protobuf:"bytes,4,opt,name=serviceStatus"` +} + +// +genclient +// +genclient:noStatus +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SeldonDeployment is the Schema for the seldondeployments API +// +k8s:openapi-gen=true +// +kubebuilder:resource:shortName=sdep +// +kubebuilder:subresource:status +type SeldonDeployment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SeldonDeploymentSpec `json:"spec,omitempty"` + Status SeldonDeploymentStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SeldonDeploymentList contains a list of SeldonDeployment +type SeldonDeploymentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SeldonDeployment `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SeldonDeployment{}, &SeldonDeploymentList{}) +} diff --git a/operator/api/v1alpha3/seldondeployment_webhook.go b/operator/api/v1alpha3/seldondeployment_webhook.go new file mode 100644 index 0000000000..eba4516b43 --- /dev/null +++ b/operator/api/v1alpha3/seldondeployment_webhook.go @@ -0,0 +1,537 @@ +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +package v1alpha3 + +import ( + "context" + "encoding/json" + "fmt" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + k8types "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + "os" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "strconv" +) + +var ( + // log is for logging in this package. + seldondeploymentlog = logf.Log.WithName("seldondeployment") + ControllerNamespace = GetEnv("POD_NAMESPACE", "seldon-system") + ControllerConfigMapName = "seldon-config" + C client.Client +) + +const PredictorServerConfigMapKeyName = "predictor_servers" + +type PredictorImageConfig struct { + ContainerImage string `json:"image"` + DefaultImageVersion string `json:"defaultImageVersion"` +} + +type PredictorServerConfig struct { + Tensorflow bool `json:"tensorflow,omitempty"` + TensorflowImage string `json:"tfImage,omitempty"` + RestConfig PredictorImageConfig `json:"rest,omitempty"` + GrpcConfig PredictorImageConfig `json:"grpc,omitempty"` +} + +// Get an environment variable given by key or return the fallback. +func GetEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func getPredictorServerConfigs() (map[string]PredictorServerConfig, error) { + configMap := &corev1.ConfigMap{} + + err := C.Get(context.TODO(), k8types.NamespacedName{Name: ControllerConfigMapName, Namespace: ControllerNamespace}, configMap) + + if err != nil { + fmt.Println("Failed to find config map " + ControllerConfigMapName) + fmt.Println(err) + return nil, err + } + return getPredictorServerConfigsFromMap(configMap) +} + +func getPredictorServerConfigsFromMap(configMap *corev1.ConfigMap) (map[string]PredictorServerConfig, error) { + predictorServerConfig := make(map[string]PredictorServerConfig) + if predictorConfig, ok := configMap.Data[PredictorServerConfigMapKeyName]; ok { + err := json.Unmarshal([]byte(predictorConfig), &predictorServerConfig) + if err != nil { + panic(fmt.Errorf("Unable to unmarshall %v json string due to %v ", PredictorServerConfigMapKeyName, err)) + } + } + + return predictorServerConfig, nil +} + +func (r *SeldonDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error { + C = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +var _ webhook.Defaulter = &SeldonDeployment{} + +func GetContainerForPredictiveUnit(p *PredictorSpec, name string) *corev1.Container { + for j := 0; j < len(p.ComponentSpecs); j++ { + cSpec := p.ComponentSpecs[j] + for k := 0; k < len(cSpec.Spec.Containers); k++ { + c := &cSpec.Spec.Containers[k] + if c.Name == name { + return c + } + } + } + return nil +} + +func GetPort(name string, ports []corev1.ContainerPort) *corev1.ContainerPort { + for i := 0; i < len(ports); i++ { + if ports[i].Name == name { + return &ports[i] + } + } + return nil +} + +func IsPrepack(pu *PredictiveUnit) bool { + isPrepack := len(*pu.Implementation) > 0 && *pu.Implementation != SIMPLE_MODEL && *pu.Implementation != SIMPLE_ROUTER && *pu.Implementation != RANDOM_ABTEST && *pu.Implementation != AVERAGE_COMBINER && *pu.Implementation != UNKNOWN_IMPLEMENTATION + return isPrepack +} + +func GetPrepackServerConfig(serverName string) PredictorServerConfig { + ServersConfigs, err := getPredictorServerConfigs() + + if err != nil { + seldondeploymentlog.Error(err, "Failed to read prepacked model servers from configmap") + } + ServerConfig, ok := ServersConfigs[serverName] + if !ok { + seldondeploymentlog.Error(nil, "No entry in predictors map for "+serverName) + } + return ServerConfig +} + +func SetImageNameForPrepackContainer(pu *PredictiveUnit, c *corev1.Container) { + //Add missing fields + // Add image + if c.Image == "" { + + ServerConfig := GetPrepackServerConfig(string(*pu.Implementation)) + + if pu.Endpoint.Type == REST { + c.Image = ServerConfig.RestConfig.ContainerImage + ":" + ServerConfig.RestConfig.DefaultImageVersion + } else { + c.Image = ServerConfig.GrpcConfig.ContainerImage + ":" + ServerConfig.GrpcConfig.DefaultImageVersion + } + + } +} + +// ----- + +func addDefaultsToGraph(pu *PredictiveUnit) { + if pu.Type == nil { + ty := UNKNOWN_TYPE + pu.Type = &ty + } + if pu.Implementation == nil { + im := UNKNOWN_IMPLEMENTATION + pu.Implementation = &im + } + for i := 0; i < len(pu.Children); i++ { + addDefaultsToGraph(&pu.Children[i]) + } +} + +func getUpdatePortNumMap(name string, nextPortNum *int32, portMap map[string]int32) int32 { + if _, present := portMap[name]; !present { + portMap[name] = *nextPortNum + *nextPortNum++ + } + return portMap[name] +} + +func (r *SeldonDeployment) DefaultSeldonDeployment() { + + var firstPuPortNum int32 = 9000 + if env_preditive_unit_service_port, ok := os.LookupEnv("PREDICTIVE_UNIT_SERVICE_PORT"); ok { + portNum, err := strconv.Atoi(env_preditive_unit_service_port) + if err != nil { + seldondeploymentlog.Error(err, "Failed to decode PREDICTIVE_UNIT_SERVICE_PORT will use default 9000", "value", env_preditive_unit_service_port) + } else { + firstPuPortNum = int32(portNum) + } + } + nextPortNum := firstPuPortNum + + portMap := map[string]int32{} + + if r.ObjectMeta.Namespace == "" { + r.ObjectMeta.Namespace = "default" + } + + for i := 0; i < len(r.Spec.Predictors); i++ { + p := r.Spec.Predictors[i] + if p.Graph.Type == nil { + ty := UNKNOWN_TYPE + p.Graph.Type = &ty + } + // Add version label for predictor if not present + if p.Labels == nil { + p.Labels = map[string]string{} + } + if _, present := p.Labels["version"]; !present { + p.Labels["version"] = p.Name + } + addDefaultsToGraph(p.Graph) + + r.Spec.Predictors[i] = p + + for j := 0; j < len(p.ComponentSpecs); j++ { + cSpec := r.Spec.Predictors[i].ComponentSpecs[j] + + // add service details for each container - looping this way as if containers in same pod and its the engine pod both need to be localhost + for k := 0; k < len(cSpec.Spec.Containers); k++ { + con := &cSpec.Spec.Containers[k] + + getUpdatePortNumMap(con.Name, &nextPortNum, portMap) + + portNum := portMap[con.Name] + + pu := GetPredictiveUnit(p.Graph, con.Name) + + if pu != nil { + + if pu.Endpoint == nil { + pu.Endpoint = &Endpoint{Type: REST} + } + var portType string + if pu.Endpoint.Type == GRPC { + portType = "grpc" + } else { + portType = "http" + } + + if con != nil { + existingPort := GetPort(portType, con.Ports) + if existingPort != nil { + portNum = existingPort.ContainerPort + } + + volFound := false + for _, vol := range con.VolumeMounts { + if vol.Name == PODINFO_VOLUME_NAME { + volFound = true + } + } + if !volFound { + con.VolumeMounts = append(con.VolumeMounts, corev1.VolumeMount{ + Name: PODINFO_VOLUME_NAME, + MountPath: PODINFO_VOLUME_PATH, + }) + } + } + + // Set ports and hostname in predictive unit so engine can read it from SDep + // if this is the first componentSpec then it's the one to put the engine in - note using outer loop counter here + if _, hasSeparateEnginePod := r.Spec.Annotations[ANNOTATION_SEPARATE_ENGINE]; j == 0 && !hasSeparateEnginePod { + pu.Endpoint.ServiceHost = "localhost" + } else { + containerServiceValue := GetContainerServiceName(r, p, con) + pu.Endpoint.ServiceHost = containerServiceValue + "." + r.ObjectMeta.Namespace + ".svc.cluster.local." + } + pu.Endpoint.ServicePort = portNum + } + } + + // Add defaultMode to volumes if not set to ensure no changes when comparing later in controller + for k := 0; k < len(cSpec.Spec.Volumes); k++ { + vol := &cSpec.Spec.Volumes[k] + if vol.Secret != nil && vol.Secret.DefaultMode == nil { + var defaultMode = corev1.SecretVolumeSourceDefaultMode + vol.Secret.DefaultMode = &defaultMode + } else if vol.ConfigMap != nil && vol.ConfigMap.DefaultMode == nil { + var defaultMode = corev1.ConfigMapVolumeSourceDefaultMode + vol.ConfigMap.DefaultMode = &defaultMode + } else if vol.DownwardAPI != nil && vol.DownwardAPI.DefaultMode == nil { + var defaultMode = corev1.DownwardAPIVolumeSourceDefaultMode + vol.DownwardAPI.DefaultMode = &defaultMode + } else if vol.Projected != nil && vol.Projected.DefaultMode == nil { + var defaultMode = corev1.ProjectedVolumeSourceDefaultMode + vol.Projected.DefaultMode = &defaultMode + } + } + } + + pus := GetPredictiveUnitList(p.Graph) + + //some pus might not have a container spec so pick those up + for l := 0; l < len(pus); l++ { + pu := pus[l] + + if IsPrepack(pu) { + + con := GetContainerForPredictiveUnit(&p, pu.Name) + + existing := con != nil + if !existing { + con = &corev1.Container{ + Name: pu.Name, + VolumeMounts: []corev1.VolumeMount{ + { + Name: PODINFO_VOLUME_NAME, + MountPath: PODINFO_VOLUME_PATH, + }, + }, + } + } + + // Add a default REST endpoint if none provided + // pu needs to have an endpoint as engine reads it from SDep in order to direct graph traffic + // probes etc will be added later by controller + if pu.Endpoint == nil { + pu.Endpoint = &Endpoint{Type: REST} + } + var portType string + if pu.Endpoint.Type == GRPC { + portType = "grpc" + } else { + portType = "http" + } + + SetImageNameForPrepackContainer(pu, con) + + // if new Add container to componentSpecs + if !existing { + if len(p.ComponentSpecs) > 0 { + p.ComponentSpecs[0].Spec.Containers = append(p.ComponentSpecs[0].Spec.Containers, *con) + } else { + podSpec := SeldonPodSpec{ + Metadata: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{*con}, + }, + } + p.ComponentSpecs = []*SeldonPodSpec{&podSpec} + + // p is a copy so update the entry + r.Spec.Predictors[i] = p + } + } + + getUpdatePortNumMap(con.Name, &nextPortNum, portMap) + portNum := portMap[pu.Name] + + if con != nil { + existingPort := GetPort(portType, con.Ports) + if existingPort != nil { + portNum = existingPort.ContainerPort + } + + volFound := false + for _, vol := range con.VolumeMounts { + if vol.Name == PODINFO_VOLUME_NAME { + volFound = true + } + } + if !volFound { + con.VolumeMounts = append(con.VolumeMounts, corev1.VolumeMount{ + Name: PODINFO_VOLUME_NAME, + MountPath: PODINFO_VOLUME_PATH, + }) + } + } + // Set ports and hostname in predictive unit so engine can read it from SDep + // if this is the firstPuPortNum then we've not added engine yet so put the engine in here + if pu.Endpoint.ServiceHost == "" { + if _, hasSeparateEnginePod := r.Spec.Annotations[ANNOTATION_SEPARATE_ENGINE]; !hasSeparateEnginePod { + pu.Endpoint.ServiceHost = "localhost" + } else { + containerServiceValue := GetContainerServiceName(r, p, con) + pu.Endpoint.ServiceHost = containerServiceValue + "." + r.ObjectMeta.Namespace + ".svc.cluster.local." + } + } + if pu.Endpoint.ServicePort == 0 { + pu.Endpoint.ServicePort = portNum + } + + } + + } + + } + +} + +// ----- + +// --- Validating + +// Check the predictive units to ensure the graph matches up with defined containers. +func checkPredictiveUnits(pu *PredictiveUnit, p *PredictorSpec, fldPath *field.Path, allErrs field.ErrorList) field.ErrorList { + if *pu.Implementation == UNKNOWN_IMPLEMENTATION { + + if GetContainerForPredictiveUnit(p, pu.Name) == nil { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "Can't find container for Predictive Unit")) + } + + if *pu.Type == UNKNOWN_TYPE && (pu.Methods == nil || len(*pu.Methods) == 0) { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "Predictive Unit has no implementation methods defined. Change to a known type or add what methods it defines")) + } + + } else if IsPrepack(pu) { + if pu.ModelURI == "" { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "Predictive unit modelUri required when using standalone servers")) + } + c := GetContainerForPredictiveUnit(p, pu.Name) + + if c == nil || c.Image == "" { + + ServersConfigs, err := getPredictorServerConfigs() + + if err != nil { + seldondeploymentlog.Error(err, "Failed to read prepacked model servers from configmap") + } + + _, ok := ServersConfigs[string(*pu.Implementation)] + if !ok { + allErrs = append(allErrs, field.Invalid(fldPath, pu.Name, "No entry in predictors map for "+string(*pu.Implementation))) + } + } + } + + for i := 0; i < len(pu.Children); i++ { + allErrs = checkPredictiveUnits(&pu.Children[i], p, fldPath.Index(i), allErrs) + } + + return allErrs +} + +func checkTraffic(mlDep *SeldonDeployment, fldPath *field.Path, allErrs field.ErrorList) field.ErrorList { + var trafficSum int32 = 0 + var shadows int = 0 + for i := 0; i < len(mlDep.Spec.Predictors); i++ { + p := mlDep.Spec.Predictors[i] + trafficSum = trafficSum + p.Traffic + + if p.Shadow == true { + shadows += 1 + } + } + if trafficSum != 100 && (len(mlDep.Spec.Predictors)-shadows) > 1 { + allErrs = append(allErrs, field.Invalid(fldPath, mlDep.Name, "Traffic must sum to 100 for multiple predictors")) + } + if trafficSum > 0 && trafficSum < 100 && len(mlDep.Spec.Predictors) == 1 { + allErrs = append(allErrs, field.Invalid(fldPath, mlDep.Name, "Traffic must sum be 100 for a single predictor when set")) + } + + 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")) + } + predictorNames[p.Name] = true + allErrs = checkPredictiveUnits(p.Graph, &p, field.NewPath("spec").Child("predictors").Index(i).Child("graph"), allErrs) + } + + allErrs = checkTraffic(r, field.NewPath("spec"), allErrs) + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + schema.GroupKind{Group: "machinelearing.seldon.io", Kind: "SeldonDeployment"}, + r.Name, allErrs) + +} + +/// --- + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-machinelearning-seldon-io-v1alpha3-seldondeployment,mutating=true,failurePolicy=fail,groups=machinelearning.seldon.io,resources=seldondeployments,verbs=create;update,versions=v1alpha3,name=mseldondeployment.kb.io + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *SeldonDeployment) Default() { + seldondeploymentlog.Info("Defaulting web hook called", "name", r.Name) + + r.DefaultSeldonDeployment() +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// +kubebuilder:webhook:verbs=create;update,path=/validate-machinelearning-seldon-io-v1alpha3-seldondeployment,mutating=false,failurePolicy=fail,groups=machinelearning.seldon.io,resources=seldondeployments,versions=v1alpha3,name=vseldondeployment.kb.io + +var _ webhook.Validator = &SeldonDeployment{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *SeldonDeployment) ValidateCreate() error { + seldondeploymentlog.Info("Validating Webhook called for CREATE", "name", r.Name) + + return r.validateSeldonDeployment() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *SeldonDeployment) ValidateUpdate(old runtime.Object) error { + seldondeploymentlog.Info("Validating webhook called for UPDATE", "name", r.Name) + + return r.validateSeldonDeployment() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *SeldonDeployment) ValidateDelete() error { + seldondeploymentlog.Info("Validating webhook called for DELETE", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil +} diff --git a/operator/api/v1alpha3/zz_generated.deepcopy.go b/operator/api/v1alpha3/zz_generated.deepcopy.go new file mode 100644 index 0000000000..0e23a55b2d --- /dev/null +++ b/operator/api/v1alpha3/zz_generated.deepcopy.go @@ -0,0 +1,443 @@ +// +build !ignore_autogenerated + +/* +Copyright 2019 The Seldon Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha3 + +import ( + "k8s.io/api/autoscaling/v2beta1" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatus. +func (in *DeploymentStatus) DeepCopy() *DeploymentStatus { + if in == nil { + return nil + } + out := new(DeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Endpoint) DeepCopyInto(out *Endpoint) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint. +func (in *Endpoint) DeepCopy() *Endpoint { + if in == nil { + return nil + } + out := new(Endpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Explainer) DeepCopyInto(out *Explainer) { + *out = *in + in.ContainerSpec.DeepCopyInto(&out.ContainerSpec) + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Endpoint != nil { + in, out := &in.Endpoint, &out.Endpoint + *out = new(Endpoint) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Explainer. +func (in *Explainer) DeepCopy() *Explainer { + if in == nil { + return nil + } + out := new(Explainer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictiveUnit) DeepCopyInto(out *PredictiveUnit) { + *out = *in + if in.Children != nil { + in, out := &in.Children, &out.Children + *out = make([]PredictiveUnit, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(PredictiveUnitType) + **out = **in + } + if in.Implementation != nil { + in, out := &in.Implementation, &out.Implementation + *out = new(PredictiveUnitImplementation) + **out = **in + } + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = new([]PredictiveUnitMethod) + if **in != nil { + in, out := *in, *out + *out = make([]PredictiveUnitMethod, len(*in)) + copy(*out, *in) + } + } + if in.Endpoint != nil { + in, out := &in.Endpoint, &out.Endpoint + *out = new(Endpoint) + **out = **in + } + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictiveUnit. +func (in *PredictiveUnit) DeepCopy() *PredictiveUnit { + if in == nil { + return nil + } + out := new(PredictiveUnit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictorImageConfig) DeepCopyInto(out *PredictorImageConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictorImageConfig. +func (in *PredictorImageConfig) DeepCopy() *PredictorImageConfig { + if in == nil { + return nil + } + out := new(PredictorImageConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictorServerConfig) DeepCopyInto(out *PredictorServerConfig) { + *out = *in + out.RestConfig = in.RestConfig + out.GrpcConfig = in.GrpcConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictorServerConfig. +func (in *PredictorServerConfig) DeepCopy() *PredictorServerConfig { + if in == nil { + return nil + } + out := new(PredictorServerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PredictorSpec) DeepCopyInto(out *PredictorSpec) { + *out = *in + if in.Graph != nil { + in, out := &in.Graph, &out.Graph + *out = new(PredictiveUnit) + (*in).DeepCopyInto(*out) + } + if in.ComponentSpecs != nil { + in, out := &in.ComponentSpecs, &out.ComponentSpecs + *out = make([]*SeldonPodSpec, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(SeldonPodSpec) + (*in).DeepCopyInto(*out) + } + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.EngineResources.DeepCopyInto(&out.EngineResources) + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.SvcOrchSpec.DeepCopyInto(&out.SvcOrchSpec) + in.Explainer.DeepCopyInto(&out.Explainer) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictorSpec. +func (in *PredictorSpec) DeepCopy() *PredictorSpec { + if in == nil { + return nil + } + out := new(PredictorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeployment) DeepCopyInto(out *SeldonDeployment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeployment. +func (in *SeldonDeployment) DeepCopy() *SeldonDeployment { + if in == nil { + return nil + } + out := new(SeldonDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeldonDeployment) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeploymentList) DeepCopyInto(out *SeldonDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SeldonDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeploymentList. +func (in *SeldonDeploymentList) DeepCopy() *SeldonDeploymentList { + if in == nil { + return nil + } + out := new(SeldonDeploymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeldonDeploymentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeploymentSpec) DeepCopyInto(out *SeldonDeploymentSpec) { + *out = *in + if in.Predictors != nil { + in, out := &in.Predictors, &out.Predictors + *out = make([]PredictorSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeploymentSpec. +func (in *SeldonDeploymentSpec) DeepCopy() *SeldonDeploymentSpec { + if in == nil { + return nil + } + out := new(SeldonDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonDeploymentStatus) DeepCopyInto(out *SeldonDeploymentStatus) { + *out = *in + if in.DeploymentStatus != nil { + in, out := &in.DeploymentStatus, &out.DeploymentStatus + *out = make(map[string]DeploymentStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ServiceStatus != nil { + in, out := &in.ServiceStatus, &out.ServiceStatus + *out = make(map[string]ServiceStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonDeploymentStatus. +func (in *SeldonDeploymentStatus) DeepCopy() *SeldonDeploymentStatus { + if in == nil { + return nil + } + out := new(SeldonDeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonHpaSpec) DeepCopyInto(out *SeldonHpaSpec) { + *out = *in + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int32) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]v2beta1.MetricSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonHpaSpec. +func (in *SeldonHpaSpec) DeepCopy() *SeldonHpaSpec { + if in == nil { + return nil + } + out := new(SeldonHpaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeldonPodSpec) DeepCopyInto(out *SeldonPodSpec) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + in.Spec.DeepCopyInto(&out.Spec) + if in.HpaSpec != nil { + in, out := &in.HpaSpec, &out.HpaSpec + *out = new(SeldonHpaSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeldonPodSpec. +func (in *SeldonPodSpec) DeepCopy() *SeldonPodSpec { + if in == nil { + return nil + } + out := new(SeldonPodSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceStatus. +func (in *ServiceStatus) DeepCopy() *ServiceStatus { + if in == nil { + return nil + } + out := new(ServiceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SvcOrchSpec) DeepCopyInto(out *SvcOrchSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]*v1.EnvVar, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(v1.EnvVar) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SvcOrchSpec. +func (in *SvcOrchSpec) DeepCopy() *SvcOrchSpec { + if in == nil { + return nil + } + out := new(SvcOrchSpec) + in.DeepCopyInto(out) + return out +} From 35a87ea8ccf216c836aadea46822e0957c7f6554 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Tue, 10 Dec 2019 17:51:35 +0000 Subject: [PATCH 05/12] Update CRD an webhook manifest --- ...elearning.seldon.io_seldondeployments.yaml | 10 ++- operator/config/webhook/manifests.yaml | 72 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/operator/config/crd/bases/machinelearning.seldon.io_seldondeployments.yaml b/operator/config/crd/bases/machinelearning.seldon.io_seldondeployments.yaml index 44214868be..a2ce895ae7 100644 --- a/operator/config/crd/bases/machinelearning.seldon.io_seldondeployments.yaml +++ b/operator/config/crd/bases/machinelearning.seldon.io_seldondeployments.yaml @@ -6073,11 +6073,17 @@ spec: type: string type: object type: object - version: v1alpha2 + version: v1 versions: - - name: v1alpha2 + - name: v1 served: true storage: true + - name: v1alpha2 + served: true + storage: false + - name: v1alpha3 + served: true + storage: false status: acceptedNames: kind: "" diff --git a/operator/config/webhook/manifests.yaml b/operator/config/webhook/manifests.yaml index 97c42bbaaf..1c090135b1 100644 --- a/operator/config/webhook/manifests.yaml +++ b/operator/config/webhook/manifests.yaml @@ -6,6 +6,24 @@ metadata: creationTimestamp: null name: mutating-webhook-configuration webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-machinelearning-seldon-io-v1-seldondeployment + failurePolicy: Fail + name: mseldondeployment.kb.io + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments - clientConfig: caBundle: Cg== service: @@ -24,6 +42,24 @@ webhooks: - UPDATE resources: - seldondeployments +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-machinelearning-seldon-io-v1alpha3-seldondeployment + failurePolicy: Fail + name: mseldondeployment.kb.io + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments --- apiVersion: admissionregistration.k8s.io/v1beta1 @@ -32,6 +68,24 @@ metadata: creationTimestamp: null name: validating-webhook-configuration webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-machinelearning-seldon-io-v1-seldondeployment + failurePolicy: Fail + name: vseldondeployment.kb.io + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments - clientConfig: caBundle: Cg== service: @@ -50,3 +104,21 @@ webhooks: - UPDATE resources: - seldondeployments +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-machinelearning-seldon-io-v1alpha3-seldondeployment + failurePolicy: Fail + name: vseldondeployment.kb.io + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments From 8107e2c83107fbf1819a58fd31fcd7e17049b1b8 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Wed, 11 Dec 2019 09:19:27 +0000 Subject: [PATCH 06/12] Add new versions to PROJECT --- operator/PROJECT | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/operator/PROJECT b/operator/PROJECT index a62b9ec741..1b8ac8d573 100644 --- a/operator/PROJECT +++ b/operator/PROJECT @@ -5,3 +5,9 @@ resources: - group: machinelearning version: v1alpha2 kind: SeldonDeployment +- group: machinelearning + version: v1alpha3 + kind: SeldonDeployment +- group: machinelearning + version: v1 + kind: SeldonDeployment From 5cfe2cbc31c22080406e281b7f2626f3ae19c71e Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Wed, 11 Dec 2019 13:27:30 +0000 Subject: [PATCH 07/12] Apply changes to api/v1 and api/v1alpha3 --- operator/api/v1/seldondeployment_types.go | 8 ++------ operator/api/v1/seldondeployment_webhook.go | 18 ------------------ .../api/v1alpha3/seldondeployment_types.go | 8 ++------ .../api/v1alpha3/seldondeployment_webhook.go | 18 ------------------ 4 files changed, 4 insertions(+), 48 deletions(-) diff --git a/operator/api/v1/seldondeployment_types.go b/operator/api/v1/seldondeployment_types.go index 03bd220ca6..aefeb818e7 100644 --- a/operator/api/v1/seldondeployment_types.go +++ b/operator/api/v1/seldondeployment_types.go @@ -19,12 +19,11 @@ package v1 import ( "crypto/md5" "encoding/hex" - "regexp" - "strings" - autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "regexp" + "strings" ) const ( @@ -360,11 +359,8 @@ type SeldonDeploymentStatus struct { ServiceStatus map[string]ServiceStatus `json:"serviceStatus,omitempty" protobuf:"bytes,4,opt,name=serviceStatus"` } -// +genclient -// +genclient:noStatus // +kubebuilder:object:root=true // +kubebuilder:storageversion -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // SeldonDeployment is the Schema for the seldondeployments API // +k8s:openapi-gen=true diff --git a/operator/api/v1/seldondeployment_webhook.go b/operator/api/v1/seldondeployment_webhook.go index d24a3a6b04..e88409a076 100644 --- a/operator/api/v1/seldondeployment_webhook.go +++ b/operator/api/v1/seldondeployment_webhook.go @@ -271,24 +271,6 @@ func (r *SeldonDeployment) DefaultSeldonDeployment() { pu.Endpoint.ServicePort = portNum } } - - // Add defaultMode to volumes if not set to ensure no changes when comparing later in controller - for k := 0; k < len(cSpec.Spec.Volumes); k++ { - vol := &cSpec.Spec.Volumes[k] - if vol.Secret != nil && vol.Secret.DefaultMode == nil { - var defaultMode = corev1.SecretVolumeSourceDefaultMode - vol.Secret.DefaultMode = &defaultMode - } else if vol.ConfigMap != nil && vol.ConfigMap.DefaultMode == nil { - var defaultMode = corev1.ConfigMapVolumeSourceDefaultMode - vol.ConfigMap.DefaultMode = &defaultMode - } else if vol.DownwardAPI != nil && vol.DownwardAPI.DefaultMode == nil { - var defaultMode = corev1.DownwardAPIVolumeSourceDefaultMode - vol.DownwardAPI.DefaultMode = &defaultMode - } else if vol.Projected != nil && vol.Projected.DefaultMode == nil { - var defaultMode = corev1.ProjectedVolumeSourceDefaultMode - vol.Projected.DefaultMode = &defaultMode - } - } } pus := GetPredictiveUnitList(p.Graph) diff --git a/operator/api/v1alpha3/seldondeployment_types.go b/operator/api/v1alpha3/seldondeployment_types.go index 15c64bf104..87c454573d 100644 --- a/operator/api/v1alpha3/seldondeployment_types.go +++ b/operator/api/v1alpha3/seldondeployment_types.go @@ -19,12 +19,11 @@ package v1alpha3 import ( "crypto/md5" "encoding/hex" - "regexp" - "strings" - autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "regexp" + "strings" ) const ( @@ -360,10 +359,7 @@ type SeldonDeploymentStatus struct { ServiceStatus map[string]ServiceStatus `json:"serviceStatus,omitempty" protobuf:"bytes,4,opt,name=serviceStatus"` } -// +genclient -// +genclient:noStatus // +kubebuilder:object:root=true -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // SeldonDeployment is the Schema for the seldondeployments API // +k8s:openapi-gen=true diff --git a/operator/api/v1alpha3/seldondeployment_webhook.go b/operator/api/v1alpha3/seldondeployment_webhook.go index eba4516b43..23def03c8a 100644 --- a/operator/api/v1alpha3/seldondeployment_webhook.go +++ b/operator/api/v1alpha3/seldondeployment_webhook.go @@ -271,24 +271,6 @@ func (r *SeldonDeployment) DefaultSeldonDeployment() { pu.Endpoint.ServicePort = portNum } } - - // Add defaultMode to volumes if not set to ensure no changes when comparing later in controller - for k := 0; k < len(cSpec.Spec.Volumes); k++ { - vol := &cSpec.Spec.Volumes[k] - if vol.Secret != nil && vol.Secret.DefaultMode == nil { - var defaultMode = corev1.SecretVolumeSourceDefaultMode - vol.Secret.DefaultMode = &defaultMode - } else if vol.ConfigMap != nil && vol.ConfigMap.DefaultMode == nil { - var defaultMode = corev1.ConfigMapVolumeSourceDefaultMode - vol.ConfigMap.DefaultMode = &defaultMode - } else if vol.DownwardAPI != nil && vol.DownwardAPI.DefaultMode == nil { - var defaultMode = corev1.DownwardAPIVolumeSourceDefaultMode - vol.DownwardAPI.DefaultMode = &defaultMode - } else if vol.Projected != nil && vol.Projected.DefaultMode == nil { - var defaultMode = corev1.ProjectedVolumeSourceDefaultMode - vol.Projected.DefaultMode = &defaultMode - } - } } pus := GetPredictiveUnitList(p.Graph) From 915b25c08fcee25bbc91d2560277b1e30a4766c6 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Wed, 11 Dec 2019 13:37:17 +0000 Subject: [PATCH 08/12] Update helm charts --- ...deployments.machinelearning.seldon.io.yaml | 10 +- .../templates/webhook.yaml | 104 +++++++++++++++--- 2 files changed, 96 insertions(+), 18 deletions(-) diff --git a/helm-charts/seldon-core-operator/templates/customresourcedefinition_seldondeployments.machinelearning.seldon.io.yaml b/helm-charts/seldon-core-operator/templates/customresourcedefinition_seldondeployments.machinelearning.seldon.io.yaml index 9c2d13f6be..299ae596df 100644 --- a/helm-charts/seldon-core-operator/templates/customresourcedefinition_seldondeployments.machinelearning.seldon.io.yaml +++ b/helm-charts/seldon-core-operator/templates/customresourcedefinition_seldondeployments.machinelearning.seldon.io.yaml @@ -2939,11 +2939,17 @@ spec: type: string type: object type: object - version: v1alpha2 + version: v1 versions: - - name: v1alpha2 + - name: v1 served: true storage: true + - name: v1alpha2 + served: true + storage: false + - name: v1alpha3 + served: true + storage: false status: acceptedNames: kind: '' diff --git a/helm-charts/seldon-core-operator/templates/webhook.yaml b/helm-charts/seldon-core-operator/templates/webhook.yaml index e0a1b7d9a5..07fe062ac7 100644 --- a/helm-charts/seldon-core-operator/templates/webhook.yaml +++ b/helm-charts/seldon-core-operator/templates/webhook.yaml @@ -21,7 +21,7 @@ webhooks: service: name: seldon-webhook-service namespace: '{{ .Release.Namespace }}' - path: /validate-machinelearning-seldon-io-v1alpha2-seldondeployment + path: /validate-machinelearning-seldon-io-v1-seldondeployment failurePolicy: Fail name: vseldondeployment.kb.io {{- if semverCompare ">=1.15.0" .Capabilities.KubeVersion.Version }} @@ -50,6 +50,24 @@ webhooks: matchLabels: seldon.io/controller-id: {{ .Values.controllerId }} {{- end }} + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments +- clientConfig: + caBundle: Cg== + service: + name: seldon-webhook-service + namespace: seldon-system + path: /validate-machinelearning-seldon-io-v1alpha2-seldondeployment + failurePolicy: Fail + name: vseldondeployment.kb.io rules: - apiGroups: - machinelearning.seldon.io @@ -60,6 +78,38 @@ webhooks: - UPDATE resources: - seldondeployments +- clientConfig: + caBundle: Cg== + service: + name: seldon-webhook-service + namespace: seldon-system + path: /validate-machinelearning-seldon-io-v1alpha3-seldondeployment + failurePolicy: Fail + name: vseldondeployment.kb.io + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments +--- + +{{- if not .Values.certManager.enabled -}} +apiVersion: v1 +data: + ca.crt: '{{ $ca.Cert | b64enc }}' + tls.crt: '{{ $cert.Cert | b64enc }}' + tls.key: '{{ $cert.Key | b64enc }}' +kind: Secret +metadata: + name: seldon-webhook-server-cert + namespace: '{{ .Release.Namespace }}' +type: kubernetes.io/tls +{{- end }} --- apiVersion: admissionregistration.k8s.io/v1beta1 @@ -80,7 +130,7 @@ webhooks: service: name: seldon-webhook-service namespace: '{{ .Release.Namespace }}' - path: /mutate-machinelearning-seldon-io-v1alpha2-seldondeployment + path: /mutate-machinelearning-seldon-io-v1-seldondeployment failurePolicy: Fail name: mseldondeployment.kb.io {{- if semverCompare ">=1.15.0" .Capabilities.KubeVersion.Version }} @@ -109,6 +159,24 @@ webhooks: matchLabels: seldon.io/controller-id: {{ .Values.controllerId }} {{- end }} + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments +- clientConfig: + caBundle: Cg== + service: + name: seldon-webhook-service + namespace: seldon-system + path: /mutate-machinelearning-seldon-io-v1alpha2-seldondeployment + failurePolicy: Fail + name: mseldondeployment.kb.io rules: - apiGroups: - machinelearning.seldon.io @@ -119,17 +187,21 @@ webhooks: - UPDATE resources: - seldondeployments ---- - -{{- if not .Values.certManager.enabled -}} -apiVersion: v1 -data: - ca.crt: '{{ $ca.Cert | b64enc }}' - tls.crt: '{{ $cert.Cert | b64enc }}' - tls.key: '{{ $cert.Key | b64enc }}' -kind: Secret -metadata: - name: seldon-webhook-server-cert - namespace: '{{ .Release.Namespace }}' -type: kubernetes.io/tls -{{- end }} +- clientConfig: + caBundle: Cg== + service: + name: seldon-webhook-service + namespace: seldon-system + path: /mutate-machinelearning-seldon-io-v1alpha3-seldondeployment + failurePolicy: Fail + name: mseldondeployment.kb.io + rules: + - apiGroups: + - machinelearning.seldon.io + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - seldondeployments From 29b88f3a854c927ae74313c9bcac6459b9269fc7 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Wed, 11 Dec 2019 14:11:54 +0000 Subject: [PATCH 09/12] Make apiVersion as variable --- helm-charts/seldon-single-model/templates/model.json | 2 +- helm-charts/seldon-single-model/values.yaml | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/helm-charts/seldon-single-model/templates/model.json b/helm-charts/seldon-single-model/templates/model.json index febf0fee93..f897a8fddd 100644 --- a/helm-charts/seldon-single-model/templates/model.json +++ b/helm-charts/seldon-single-model/templates/model.json @@ -1,5 +1,5 @@ { - "apiVersion": "machinelearning.seldon.io/v1alpha2", + "apiVersion": {{ .Values.apiVersion }}, "kind": "SeldonDeployment", "metadata": { "labels": {{ .Values.sdepLabels | toJson }}, diff --git a/helm-charts/seldon-single-model/values.yaml b/helm-charts/seldon-single-model/values.yaml index a9943a943a..2768100bbe 100644 --- a/helm-charts/seldon-single-model/values.yaml +++ b/helm-charts/seldon-single-model/values.yaml @@ -1,4 +1,5 @@ name: my-model +apiVersion: machinelearning.seldon.io/v1 model: image: name: seldonio/mock_classifier:1.0 @@ -26,12 +27,12 @@ engine: # Add oauth key and secret if using the default API Oauth Gateway for ingress oauth: - key: - secret: + key: + secret: sdepLabels: app: "seldon" predictorLabels: version: "v1" - fluentd: "true" \ No newline at end of file + fluentd: "true" From a437fcc5d1e6eebff23e544fd1d835df8605b291 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Wed, 11 Dec 2019 17:27:21 +0000 Subject: [PATCH 10/12] Register all types and webhooks --- operator/main.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/operator/main.go b/operator/main.go index ea7f015b5b..cfac424841 100644 --- a/operator/main.go +++ b/operator/main.go @@ -21,6 +21,8 @@ import ( "os" machinelearningv1 "github.com/seldonio/seldon-core/operator/api/v1" + machinelearningv1alpha2 "github.com/seldonio/seldon-core/operator/api/v1alpha2" + machinelearningv1alpha3 "github.com/seldonio/seldon-core/operator/api/v1alpha3" "github.com/seldonio/seldon-core/operator/controllers" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -44,6 +46,8 @@ func init() { _ = appsv1.AddToScheme(scheme) _ = corev1.AddToScheme(scheme) _ = machinelearningv1.AddToScheme(scheme) + _ = machinelearningv1alpha2.AddToScheme(scheme) + _ = machinelearningv1alpha3.AddToScheme(scheme) if controllers.GetEnv(controllers.ENV_ISTIO_ENABLED, "false") == "true" { istio.AddToScheme(scheme) } @@ -86,6 +90,18 @@ func main() { os.Exit(1) } + // Note that we need to create the webhooks for v1alpha2 and v1alpha3 because + // we are changing our storage version + if err = (&machinelearningv1alpha2.SeldonDeployment{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "SeldonDeployment") + os.Exit(1) + } + + if err = (&machinelearningv1alpha3.SeldonDeployment{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "SeldonDeployment") + os.Exit(1) + } + if err = (&machinelearningv1.SeldonDeployment{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "SeldonDeployment") os.Exit(1) From 35e8b03821452db091547bcea63242b0fa456c82 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Wed, 11 Dec 2019 17:39:15 +0000 Subject: [PATCH 11/12] Add test deploying different apiVersions --- testing/scripts/test_api_version.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 testing/scripts/test_api_version.py diff --git a/testing/scripts/test_api_version.py b/testing/scripts/test_api_version.py new file mode 100644 index 0000000000..17b144bf2d --- /dev/null +++ b/testing/scripts/test_api_version.py @@ -0,0 +1,42 @@ +import pytest +from seldon_e2e_utils import ( + wait_for_rollout, + initial_rest_request, + rest_request_ambassador, + retry_run, + API_AMBASSADOR, +) +from subprocess import run + + +@pytest.mark.parametrize( + "apiVersion", + [ + "machinelearning.seldon.io/v1alpha2", + "machinelearning.seldon.io/v1alpha3", + "machinelearning.seldon.io/v1", + ], +) +def test_api_version(apiVersion): + version = apiVersion.split("/")[-1] + namespace = f"test-api-version-{version}" + retry_run(f"kubectl create namespace {namespace}") + command = ( + "helm install mymodel ../../helm-charts/seldon-single-model " + "--set oauth.key=oauth-key " + "--set oauth.secret=oauth-secret " + f"--set apiVersion={apiVersion} " + f"--namespace {namespace}" + ) + run(command, shell=True, check=True) + + wait_for_rollout(f"mymodel-mymodel-7cd068f", namespace) + initial_rest_request("mymodel", namespace) + + r = rest_request_ambassador("mymodel", namespace, API_AMBASSADOR) + + assert r.status_code == 200 + assert len(r.json()["data"]["tensor"]["values"]) == 1 + + run(f"helm delete mymodel", shell=True) + run(f"kubectl delete namespace {namespace}", shell=True) From 645a1e10cc8147aaf04c11818bd836b7c9e6b4b5 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez-Martin Date: Wed, 11 Dec 2019 18:22:53 +0000 Subject: [PATCH 12/12] Add test updating between apiVersion --- testing/resources/graph7.json | 48 +++++++++++++++++ testing/resources/graph8.json | 48 +++++++++++++++++ testing/scripts/test_rolling_updates.py | 71 ++++++++++++++++++++++++- 3 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 testing/resources/graph7.json create mode 100644 testing/resources/graph8.json diff --git a/testing/resources/graph7.json b/testing/resources/graph7.json new file mode 100644 index 0000000000..45ebc08faa --- /dev/null +++ b/testing/resources/graph7.json @@ -0,0 +1,48 @@ +{ + "apiVersion": "machinelearning.seldon.io/v1alpha3", + "kind": "SeldonDeployment", + "metadata": { + "labels": { + "app": "seldon" + }, + "name": "mymodel" + }, + "spec": { + "name": "mymodel", + "oauth_key": "oauth-key", + "oauth_secret": "oauth-secret", + "predictors": [ + { + "componentSpecs": [ + { + "spec": { + "containers": [ + { + "image": "seldonio/fixed-model:0.1", + "imagePullPolicy": "IfNotPresent", + "name": "complex-model", + "resources": { + "requests": { + "memory": "1Mi" + } + } + } + ], + "terminationGracePeriodSeconds": 1 + } + } + ], + "graph": { + "children": [], + "name": "complex-model", + "endpoint": { + "type": "REST" + }, + "type": "MODEL" + }, + "name": "mymodel", + "replicas": 1 + } + ] + } +} diff --git a/testing/resources/graph8.json b/testing/resources/graph8.json new file mode 100644 index 0000000000..2de45c4592 --- /dev/null +++ b/testing/resources/graph8.json @@ -0,0 +1,48 @@ +{ + "apiVersion": "machinelearning.seldon.io/v1", + "kind": "SeldonDeployment", + "metadata": { + "labels": { + "app": "seldon" + }, + "name": "mymodel" + }, + "spec": { + "name": "mymodel", + "oauth_key": "oauth-key", + "oauth_secret": "oauth-secret", + "predictors": [ + { + "componentSpecs": [ + { + "spec": { + "containers": [ + { + "image": "seldonio/fixed-model:0.1", + "imagePullPolicy": "IfNotPresent", + "name": "complex-model", + "resources": { + "requests": { + "memory": "1Mi" + } + } + } + ], + "terminationGracePeriodSeconds": 1 + } + } + ], + "graph": { + "children": [], + "name": "complex-model", + "endpoint": { + "type": "REST" + }, + "type": "MODEL" + }, + "name": "mymodel", + "replicas": 1 + } + ] + } +} diff --git a/testing/scripts/test_rolling_updates.py b/testing/scripts/test_rolling_updates.py index 8fb8cdb9c1..b7621077e1 100644 --- a/testing/scripts/test_rolling_updates.py +++ b/testing/scripts/test_rolling_updates.py @@ -1,3 +1,7 @@ +import os +import time +import logging +import pytest from subprocess import run from seldon_e2e_utils import ( wait_for_rollout, @@ -6,8 +10,17 @@ retry_run, API_AMBASSADOR, ) -import time -import logging + + +def clean_string(string): + string = string.lower() + string = string.replace("_", "-") + string = string.replace(".", "-") + return string + + +def to_resources_path(file_name): + return os.path.join("..", "resources", file_name) class TestRollingHttp(object): @@ -395,3 +408,57 @@ def test_rolling_update10(self): run(f"kubectl delete -f ../resources/graph1svc.json -n {namespace}", shell=True) run(f"kubectl delete -f ../resources/graph6svc.json -n {namespace}", shell=True) run(f"kubectl delete namespace {namespace}", shell=True) + + +@pytest.mark.parametrize( + "from_deployment,to_deployment", + [ + ("graph1.json", "graph8.json"), # From v1alpha2 to v1 + ("graph7.json", "graph8.json"), # From v1alpha3 to v1 + ], +) +def test_rolling_update_deployment(from_deployment, to_deployment): + from_name = clean_string(from_deployment) + to_name = clean_string(to_deployment) + namespace = f"test-rolling-update-{from_name}-{to_name}" + retry_run(f"kubectl create namespace {namespace}") + + from_file_path = to_resources_path(from_deployment) + retry_run(f"kubectl apply -f {from_file_path} -n {namespace}") + # Note that this is not yet parametrised! + wait_for_rollout("mymodel-mymodel-e2eb561", namespace) + + logging.warning("Initial request") + r = initial_rest_request("mymodel", namespace) + assert r.status_code == 200 + assert r.json()["data"]["tensor"]["values"] == [1.0, 2.0, 3.0, 4.0] + + to_file_path = to_resources_path(to_deployment) + retry_run(f"kubectl apply -f {to_file_path} -n {namespace}") + r = initial_rest_request("mymodel", namespace) + assert r.status_code == 200 + assert r.json()["data"]["tensor"]["values"] == [1.0, 2.0, 3.0, 4.0] + + i = 0 + for i in range(100): + r = rest_request_ambassador("mymodel", namespace, API_AMBASSADOR) + assert r.status_code == 200 + res = r.json() + assert ( + res["meta"]["requestPath"]["complex-model"] == "seldonio/fixed-model:0.1" + and res["data"]["tensor"]["values"] == [1.0, 2.0, 3.0, 4.0] + ) or ( + res["meta"]["requestPath"]["complex-model"] == "seldonio/fixed-model:0.2" + and res["data"]["tensor"]["values"] == [5.0, 6.0, 7.0, 8.0] + ) + if (not r.status_code == 200) or ( + res["data"]["tensor"]["values"] == [5.0, 6.0, 7.0, 8.0] + ): + break + time.sleep(1) + + assert i < 100 + + run(f"kubectl delete -f {from_file_path} -n {namespace}", shell=True) + run(f"kubectl delete -f {to_file_path} -n {namespace}", shell=True) + run(f"kubectl delete namespace {namespace}", shell=True)