diff --git a/.gitignore b/.gitignore index 391945a9..08c515b9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ model-registry metadata.sqlite.db vendor +coverage.txt # Robot Framework files log.html diff --git a/Makefile b/Makefile index c38838e3..9024695e 100644 --- a/Makefile +++ b/Makefile @@ -112,7 +112,7 @@ bin/golangci-lint: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_BIN) v1.54.2 bin/goverter: - GOBIN=$(PROJECT_PATH)/bin go install github.com/jmattheis/goverter/cmd/goverter@v1.0.0 + GOBIN=$(PROJECT_PATH)/bin go install github.com/jmattheis/goverter/cmd/goverter@v1.1.1 OPENAPI_GENERATOR ?= ${PROJECT_BIN}/openapi-generator-cli NPM ?= "$(shell which npm)" diff --git a/internal/converter/generated/openapi_converter.gen.go b/internal/converter/generated/openapi_converter.gen.go index 8f98e9b4..f1334c9f 100755 --- a/internal/converter/generated/openapi_converter.gen.go +++ b/internal/converter/generated/openapi_converter.gen.go @@ -2,7 +2,10 @@ package generated -import openapi "github.com/opendatahub-io/model-registry/pkg/openapi" +import ( + converter "github.com/opendatahub-io/model-registry/internal/converter" + openapi "github.com/opendatahub-io/model-registry/pkg/openapi" +) type OpenAPIConverterImpl struct{} @@ -549,6 +552,126 @@ func (c *OpenAPIConverterImpl) ConvertServingEnvironmentUpdate(source *openapi.S } return pOpenapiServingEnvironment, nil } +func (c *OpenAPIConverterImpl) OverrideNotEditableForInferenceService(source converter.OpenapiUpdateWrapper[openapi.InferenceService]) (openapi.InferenceService, error) { + openapiInferenceService := converter.InitInferenceServiceWithUpdate(source) + var pString *string + if source.Existing != nil { + pString = source.Existing.Name + } + var pString2 *string + if pString != nil { + xstring := *pString + pString2 = &xstring + } + openapiInferenceService.Name = pString2 + var pString3 *string + if source.Existing != nil { + pString3 = &source.Existing.RegisteredModelId + } + var xstring2 string + if pString3 != nil { + xstring2 = *pString3 + } + openapiInferenceService.RegisteredModelId = xstring2 + var pString4 *string + if source.Existing != nil { + pString4 = &source.Existing.ServingEnvironmentId + } + var xstring3 string + if pString4 != nil { + xstring3 = *pString4 + } + openapiInferenceService.ServingEnvironmentId = xstring3 + return openapiInferenceService, nil +} +func (c *OpenAPIConverterImpl) OverrideNotEditableForModelArtifact(source converter.OpenapiUpdateWrapper[openapi.ModelArtifact]) (openapi.ModelArtifact, error) { + openapiModelArtifact := converter.InitModelArtifactWithUpdate(source) + var pString *string + if source.Existing != nil { + pString = source.Existing.Name + } + var pString2 *string + if pString != nil { + xstring := *pString + pString2 = &xstring + } + openapiModelArtifact.Name = pString2 + var pString3 *string + if source.Existing != nil { + pString3 = &source.Existing.ArtifactType + } + var xstring2 string + if pString3 != nil { + xstring2 = *pString3 + } + openapiModelArtifact.ArtifactType = xstring2 + return openapiModelArtifact, nil +} +func (c *OpenAPIConverterImpl) OverrideNotEditableForModelVersion(source converter.OpenapiUpdateWrapper[openapi.ModelVersion]) (openapi.ModelVersion, error) { + openapiModelVersion := converter.InitModelVersionWithUpdate(source) + var pString *string + if source.Existing != nil { + pString = source.Existing.Name + } + var pString2 *string + if pString != nil { + xstring := *pString + pString2 = &xstring + } + openapiModelVersion.Name = pString2 + return openapiModelVersion, nil +} +func (c *OpenAPIConverterImpl) OverrideNotEditableForRegisteredModel(source converter.OpenapiUpdateWrapper[openapi.RegisteredModel]) (openapi.RegisteredModel, error) { + openapiRegisteredModel := converter.InitRegisteredModelWithUpdate(source) + var pString *string + if source.Existing != nil { + pString = source.Existing.Name + } + var pString2 *string + if pString != nil { + xstring := *pString + pString2 = &xstring + } + openapiRegisteredModel.Name = pString2 + return openapiRegisteredModel, nil +} +func (c *OpenAPIConverterImpl) OverrideNotEditableForServeModel(source converter.OpenapiUpdateWrapper[openapi.ServeModel]) (openapi.ServeModel, error) { + openapiServeModel := converter.InitServeModelWithUpdate(source) + var pString *string + if source.Existing != nil { + pString = source.Existing.Name + } + var pString2 *string + if pString != nil { + xstring := *pString + pString2 = &xstring + } + openapiServeModel.Name = pString2 + var pString3 *string + if source.Existing != nil { + pString3 = &source.Existing.ModelVersionId + } + var xstring2 string + if pString3 != nil { + xstring2 = *pString3 + } + openapiServeModel.ModelVersionId = xstring2 + return openapiServeModel, nil +} +func (c *OpenAPIConverterImpl) OverrideNotEditableForServingEnvironment(source converter.OpenapiUpdateWrapper[openapi.ServingEnvironment]) (openapi.ServingEnvironment, error) { + openapiServingEnvironment := converter.InitServingEnvironmentWithUpdate(source) + var pString *string + if source.Existing != nil { + pString = source.Existing.Name + } + var pString2 *string + if pString != nil { + xstring := *pString + pString2 = &xstring + } + openapiServingEnvironment.Name = pString2 + return openapiServingEnvironment, nil +} func (c *OpenAPIConverterImpl) openapiMetadataValueToOpenapiMetadataValue(source openapi.MetadataValue) openapi.MetadataValue { var openapiMetadataValue openapi.MetadataValue openapiMetadataValue.MetadataBoolValue = c.pOpenapiMetadataBoolValueToPOpenapiMetadataBoolValue(source.MetadataBoolValue) diff --git a/internal/converter/opeanpi_converter.go b/internal/converter/opeanpi_converter.go index 46163b85..230fc2e4 100644 --- a/internal/converter/opeanpi_converter.go +++ b/internal/converter/opeanpi_converter.go @@ -2,6 +2,11 @@ package converter import "github.com/opendatahub-io/model-registry/pkg/openapi" +// NOTE: methods must follow these patterns, otherwise tests could not find possible issues: +// Converters createEntity to entity: ConvertCreate +// Converters updateEntity to entity: ConvertUpdate +// Converters override fields entity: OverrideNotEditableFor + // goverter:converter // goverter:output:file ./generated/openapi_converter.gen.go // goverter:wrapErrors @@ -43,4 +48,40 @@ type OpenAPIConverter interface { // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Name ModelVersionId ConvertServeModelUpdate(source *openapi.ServeModelUpdate) (*openapi.ServeModel, error) + + // Ignore all fields that ARE editable + // goverter:default InitRegisteredModelWithUpdate + // goverter:autoMap Existing + // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalID CustomProperties State + OverrideNotEditableForRegisteredModel(source OpenapiUpdateWrapper[openapi.RegisteredModel]) (openapi.RegisteredModel, error) + + // Ignore all fields that ARE editable + // goverter:default InitModelVersionWithUpdate + // goverter:autoMap Existing + // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalID CustomProperties State + OverrideNotEditableForModelVersion(source OpenapiUpdateWrapper[openapi.ModelVersion]) (openapi.ModelVersion, error) + + // Ignore all fields that ARE editable + // goverter:default InitModelArtifactWithUpdate + // goverter:autoMap Existing + // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalID CustomProperties Uri State ServiceAccountName ModelFormatName ModelFormatVersion StorageKey StoragePath + OverrideNotEditableForModelArtifact(source OpenapiUpdateWrapper[openapi.ModelArtifact]) (openapi.ModelArtifact, error) + + // Ignore all fields that ARE editable + // goverter:default InitServingEnvironmentWithUpdate + // goverter:autoMap Existing + // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalID CustomProperties + OverrideNotEditableForServingEnvironment(source OpenapiUpdateWrapper[openapi.ServingEnvironment]) (openapi.ServingEnvironment, error) + + // Ignore all fields that ARE editable + // goverter:default InitInferenceServiceWithUpdate + // goverter:autoMap Existing + // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalID CustomProperties ModelVersionId Runtime State + OverrideNotEditableForInferenceService(source OpenapiUpdateWrapper[openapi.InferenceService]) (openapi.InferenceService, error) + + // Ignore all fields that ARE editable + // goverter:default InitServeModelWithUpdate + // goverter:autoMap Existing + // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalID CustomProperties LastKnownState + OverrideNotEditableForServeModel(source OpenapiUpdateWrapper[openapi.ServeModel]) (openapi.ServeModel, error) } diff --git a/internal/converter/openapi_converter_test.go b/internal/converter/openapi_converter_test.go new file mode 100644 index 00000000..36f256f9 --- /dev/null +++ b/internal/converter/openapi_converter_test.go @@ -0,0 +1,160 @@ +package converter + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/opendatahub-io/model-registry/pkg/openapi" +) + +// visitor +type visitor struct { + t *testing.T + entities map[string]*oapiEntity +} + +func newVisitor(t *testing.T, f *ast.File) visitor { + return visitor{ + t: t, + entities: map[string]*oapiEntity{ + "RegisteredModel": { + obj: openapi.RegisteredModel{}, + }, + "ModelVersion": { + obj: openapi.ModelVersion{}, + }, + "ModelArtifact": { + obj: openapi.ModelArtifact{}, + }, + "ServingEnvironment": { + obj: openapi.ServingEnvironment{}, + }, + "InferenceService": { + obj: openapi.InferenceService{}, + }, + "ServeModel": { + obj: openapi.ServeModel{}, + }, + }, + } +} + +func (v *visitor) extractGroup(regex *regexp.Regexp, s string) string { + extracted := regex.FindStringSubmatch(s) + if len(extracted) != 2 { + v.t.Errorf("unable to extract groups from %s for %s", regex.String(), s) + } + // the first one is the wole matched string, the second one is the group + return extracted[1] +} + +func (v *visitor) getEntity(name string) *oapiEntity { + val, ok := v.entities[name] + if !ok { + v.t.Errorf("openapi entity not found in the entities map: %s", name) + } + return val +} + +func (v visitor) Visit(n ast.Node) ast.Visitor { + if n == nil { + return nil + } + + switch d := n.(type) { + case *ast.InterfaceType: + for _, m := range d.Methods.List { + methodName := m.Names[0].Name + + if converterMethodPattern.MatchString(methodName) { + entityName := v.extractGroup(converterMethodPattern, methodName) + entity := v.getEntity(entityName) + // there should be just one doc comment matching ignoreDirectivePattern + for _, c := range m.Doc.List { + if ignoreDirectivePattern.MatchString(c.Text) { + entity.notEditableFields = v.extractGroup(ignoreDirectivePattern, c.Text) + } + } + } else if overrideNotEditableMethodPattern.MatchString(methodName) { + entityName := v.extractGroup(overrideNotEditableMethodPattern, methodName) + entity := v.getEntity(entityName) + // there should be just one doc comment matching ignoreDirectivePattern + for _, c := range m.Doc.List { + if ignoreDirectivePattern.MatchString(c.Text) { + entity.ignoredFields = v.extractGroup(ignoreDirectivePattern, c.Text) + } + } + } + } + v.checkEntities() + } + return v +} + +// checkEntities check if all editable fields are listed in the goverter ignore directive of OverrideNotEditableFor +func (v *visitor) checkEntities() { + errorMsgs := map[string][]string{} + for k, v := range v.entities { + msgs := checkEntity(v) + if len(msgs) > 0 { + errorMsgs[k] = msgs + } + } + + if len(errorMsgs) > 0 { + missingFieldsMsg := "" + for k, fields := range errorMsgs { + missingFieldsMsg += fmt.Sprintf("%s: %v\n", k, fields) + } + v.t.Errorf("missing fields to be ignored for OverrideNotEditableFor* goverter methods:\n%v", missingFieldsMsg) + } +} + +// checkEntity check if there are missing fields to be ignored in the override method +func checkEntity(entity *oapiEntity) []string { + res := []string{} + objType := reflect.TypeOf(entity.obj) + for i := 0; i < objType.NumField(); i++ { + field := objType.Field(i) + if !strings.Contains(entity.notEditableFields, field.Name) && !strings.Contains(entity.ignoredFields, field.Name) { + // check if the not editable field (first check) is not present in the ignored fields (second check) + // if this condition is true, we missed that field in the Override method ignore list + res = append(res, field.Name) + } + } + return res +} + +// test + +var converterMethodPattern *regexp.Regexp = regexp.MustCompile(`Convert(?P\w+)Update`) +var overrideNotEditableMethodPattern *regexp.Regexp = regexp.MustCompile(`OverrideNotEditableFor(?P\w+)`) +var ignoreDirectivePattern *regexp.Regexp = regexp.MustCompile(`// goverter:ignore (?P.+)`) + +func TestOverrideNotEditableFields(t *testing.T) { + _ = setup(t) + + fset := token.NewFileSet() // positions are relative to fset + wd, err := os.Getwd() + if err != nil { + t.Errorf("error getting current working directory") + } + filePath := fmt.Sprintf("%s/opeanpi_converter.go", wd) + f, _ := parser.ParseFile(fset, filePath, nil, parser.ParseComments) + + v := newVisitor(t, f) + ast.Walk(v, f) +} + +type oapiEntity struct { + obj any + notEditableFields string + ignoredFields string +} diff --git a/internal/converter/openapi_converter_util.go b/internal/converter/openapi_converter_util.go new file mode 100644 index 00000000..c82bda4a --- /dev/null +++ b/internal/converter/openapi_converter_util.go @@ -0,0 +1,71 @@ +package converter + +import "github.com/opendatahub-io/model-registry/pkg/openapi" + +type OpenapiUpdateWrapper[ + M openapi.RegisteredModel | + openapi.ModelVersion | + openapi.ModelArtifact | + openapi.ServingEnvironment | + openapi.InferenceService | + openapi.ServeModel, +] struct { + Existing *M + Update *M +} + +func NewOpenapiUpdateWrapper[ + M openapi.RegisteredModel | + openapi.ModelVersion | + openapi.ModelArtifact | + openapi.ServingEnvironment | + openapi.InferenceService | + openapi.ServeModel, +](existing *M, update *M) OpenapiUpdateWrapper[M] { + return OpenapiUpdateWrapper[M]{ + Existing: existing, + Update: update, + } +} + +func InitRegisteredModelWithUpdate(source OpenapiUpdateWrapper[openapi.RegisteredModel]) openapi.RegisteredModel { + if source.Update != nil { + return *source.Update + } + return openapi.RegisteredModel{} +} + +func InitModelVersionWithUpdate(source OpenapiUpdateWrapper[openapi.ModelVersion]) openapi.ModelVersion { + if source.Update != nil { + return *source.Update + } + return openapi.ModelVersion{} +} + +func InitModelArtifactWithUpdate(source OpenapiUpdateWrapper[openapi.ModelArtifact]) openapi.ModelArtifact { + if source.Update != nil { + return *source.Update + } + return openapi.ModelArtifact{} +} + +func InitServingEnvironmentWithUpdate(source OpenapiUpdateWrapper[openapi.ServingEnvironment]) openapi.ServingEnvironment { + if source.Update != nil { + return *source.Update + } + return openapi.ServingEnvironment{} +} + +func InitInferenceServiceWithUpdate(source OpenapiUpdateWrapper[openapi.InferenceService]) openapi.InferenceService { + if source.Update != nil { + return *source.Update + } + return openapi.InferenceService{} +} + +func InitServeModelWithUpdate(source OpenapiUpdateWrapper[openapi.ServeModel]) openapi.ServeModel { + if source.Update != nil { + return *source.Update + } + return openapi.ServeModel{} +} diff --git a/pkg/core/core.go b/pkg/core/core.go index 188ea68d..0dfce13c 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" "github.com/opendatahub-io/model-registry/internal/apiutils" "github.com/opendatahub-io/model-registry/internal/converter" + "github.com/opendatahub-io/model-registry/internal/converter/generated" "github.com/opendatahub-io/model-registry/internal/mapper" "github.com/opendatahub-io/model-registry/internal/ml_metadata/proto" "github.com/opendatahub-io/model-registry/pkg/api" @@ -26,8 +27,9 @@ var ( // ModelRegistryService is the core library of the model registry type ModelRegistryService struct { - mlmdClient proto.MetadataStoreServiceClient - mapper *mapper.Mapper + mlmdClient proto.MetadataStoreServiceClient + mapper *mapper.Mapper + openapiConv *generated.OpenAPIConverterImpl } // NewModelRegistryService create a fresh instance of ModelRegistryService, taking care of setting up needed MLMD Types @@ -154,6 +156,7 @@ func NewModelRegistryService(cc grpc.ClientConnInterface) (api.ModelRegistryApi, inferenceServiceResp.GetTypeId(), serveModelResp.GetTypeId(), ), + openapiConv: &generated.OpenAPIConverterImpl{}, }, nil } @@ -171,13 +174,12 @@ func (serv *ModelRegistryService) UpsertRegisteredModel(registeredModel *openapi if err != nil { return nil, err } - } - // if already existing assure the name is the same - if existing != nil && registeredModel.Name == nil { - // user did not provide it - // need to set it to avoid mlmd error "context name should not be empty" - registeredModel.Name = existing.Name + withNotEditable, err := serv.openapiConv.OverrideNotEditableForRegisteredModel(converter.NewOpenapiUpdateWrapper(existing, registeredModel)) + if err != nil { + return nil, err + } + registeredModel = &withNotEditable } modelCtx, err := serv.mapper.MapFromRegisteredModel(registeredModel) @@ -365,17 +367,17 @@ func (serv *ModelRegistryService) UpsertModelVersion(modelVersion *openapi.Model if err != nil { return nil, err } - registeredModel, err = serv.getRegisteredModelByVersionId(*modelVersion.Id) + + withNotEditable, err := serv.openapiConv.OverrideNotEditableForModelVersion(converter.NewOpenapiUpdateWrapper(existing, modelVersion)) if err != nil { return nil, err } - } + modelVersion = &withNotEditable - // if already existing assure the name is the same - if existing != nil && modelVersion.Name == nil { - // user did not provide it - // need to set it to avoid mlmd error "context name should not be empty" - modelVersion.Name = existing.Name + registeredModel, err = serv.getRegisteredModelByVersionId(*modelVersion.Id) + if err != nil { + return nil, err + } } modelCtx, err := serv.mapper.MapFromModelVersion(modelVersion, *registeredModel.Id, registeredModel.Name) @@ -595,18 +597,16 @@ func (serv *ModelRegistryService) UpsertModelArtifact(modelArtifact *openapi.Mod if err != nil { return nil, err } - _, err = serv.getModelVersionByArtifactId(*modelArtifact.Id) + + withNotEditable, err := serv.openapiConv.OverrideNotEditableForModelArtifact(converter.NewOpenapiUpdateWrapper(existing, modelArtifact)) if err != nil { return nil, err } - } + modelArtifact = &withNotEditable - // if already existing assure the name is the same - if existing != nil { - if modelArtifact.Name == nil { - // user did not provide it - // need to set it to avoid mlmd error "artifact name should not be empty" - modelArtifact.Name = existing.Name + _, err = serv.getModelVersionByArtifactId(*modelArtifact.Id) + if err != nil { + return nil, err } } @@ -787,13 +787,12 @@ func (serv *ModelRegistryService) UpsertServingEnvironment(servingEnvironment *o if err != nil { return nil, err } - } - // if already existing assure the name is the same - if existing != nil && servingEnvironment.Name == nil { - // user did not provide it - // need to set it to avoid mlmd error "context name should not be empty" - servingEnvironment.Name = existing.Name + withNotEditable, err := serv.openapiConv.OverrideNotEditableForServingEnvironment(converter.NewOpenapiUpdateWrapper(existing, servingEnvironment)) + if err != nil { + return nil, err + } + servingEnvironment = &withNotEditable } protoCtx, err := serv.mapper.MapFromServingEnvironment(servingEnvironment) @@ -937,10 +936,18 @@ func (serv *ModelRegistryService) UpsertInferenceService(inferenceService *opena } else { // update glog.Infof("Updating InferenceService %s", *inferenceService.Id) + existing, err = serv.GetInferenceServiceById(*inferenceService.Id) if err != nil { return nil, err } + + withNotEditable, err := serv.openapiConv.OverrideNotEditableForInferenceService(converter.NewOpenapiUpdateWrapper(existing, inferenceService)) + if err != nil { + return nil, err + } + inferenceService = &withNotEditable + servingEnvironment, err = serv.getServingEnvironmentByInferenceServiceId(*inferenceService.Id) if err != nil { return nil, err @@ -1153,10 +1160,18 @@ func (serv *ModelRegistryService) UpsertServeModel(serveModel *openapi.ServeMode } else { // update glog.Infof("Updating ServeModel %s", *serveModel.Id) + existing, err = serv.GetServeModelById(*serveModel.Id) if err != nil { return nil, err } + + withNotEditable, err := serv.openapiConv.OverrideNotEditableForServeModel(converter.NewOpenapiUpdateWrapper(existing, serveModel)) + if err != nil { + return nil, err + } + serveModel = &withNotEditable + _, err = serv.getInferenceServiceByServeModel(*serveModel.Id) if err != nil { return nil, err diff --git a/pkg/core/core_test.go b/pkg/core/core_test.go index a4cd99a5..db4aba1e 100644 --- a/pkg/core/core_test.go +++ b/pkg/core/core_test.go @@ -2447,7 +2447,6 @@ func TestUpdateInferenceService(t *testing.T) { getAllResp, err := client.GetContexts(context.Background(), &proto.GetContextsRequest{}) assertion.Nilf(err, "error retrieving all contexts, not related to the test itself: %v", err) - fmt.Printf("%+v", getAllResp.Contexts) assertion.Equal(3, len(getAllResp.Contexts), "there should be 3 contexts saved in mlmd") // update with nil name @@ -2474,6 +2473,14 @@ func TestUpdateInferenceService(t *testing.T) { assertion.Equal(author, byId.Contexts[0].CustomProperties["author"].GetStringValue(), "saved author custom property should match the provided one") assertion.Equal(newScore, byId.Contexts[0].CustomProperties["score"].GetDoubleValue(), "saved score custom property should match the provided one") assertion.Equalf(*inferenceServiceTypeName, *byId.Contexts[0].Type, "saved context should be of type of %s", *inferenceServiceTypeName) + + // update with empty registeredModelId + newExternalId = "org.my_awesome_entity_@v1" + prevRegModelId := updatedEntity.RegisteredModelId + updatedEntity.RegisteredModelId = "" + updatedEntity, err = service.UpsertInferenceService(updatedEntity) + assertion.Nil(err) + assertion.Equal(prevRegModelId, updatedEntity.RegisteredModelId) } func TestUpdateInferenceServiceFailure(t *testing.T) { @@ -3096,6 +3103,13 @@ func TestUpdateServeModel(t *testing.T) { assertion.Equal(string(newState), getById.Executions[0].LastKnownState.String()) assertion.Equal(*createdVersionIdAsInt, getById.Executions[0].Properties["model_version_id"].GetIntValue()) assertion.Equal(*(*createdEntity.CustomProperties)["author"].MetadataStringValue.StringValue, getById.Executions[0].CustomProperties["author"].GetStringValue()) + + prevModelVersionId := updatedEntity.ModelVersionId + updatedEntity.ModelVersionId = "" + updatedEntity, err = service.UpsertServeModel(updatedEntity, &inferenceServiceId) + assertion.Nilf(err, "error updating entity for %d: %v", inferenceServiceId, err) + assertion.Equal(prevModelVersionId, updatedEntity.ModelVersionId) + } func TestUpdateServeModelFailure(t *testing.T) {