diff --git a/.github/workflows/build-image-pr.yml b/.github/workflows/build-image-pr.yml index c76a5a15..b0c56fff 100644 --- a/.github/workflows/build-image-pr.yml +++ b/.github/workflows/build-image-pr.yml @@ -33,7 +33,7 @@ jobs: VERSION: ${{ steps.tags.outputs.tag }} run: ./scripts/build_deploy.sh - name: Start Kind Cluster - uses: helm/kind-action@v1.9.0 + uses: helm/kind-action@v1.10.0 with: node_image: "kindest/node:v1.27.11" - name: Load Local Registry Test Image diff --git a/api/openapi/model-registry.yaml b/api/openapi/model-registry.yaml index f6503839..1b8eedb5 100644 --- a/api/openapi/model-registry.yaml +++ b/api/openapi/model-registry.yaml @@ -1033,6 +1033,8 @@ components: - $ref: "#/components/schemas/BaseResourceUpdate" - type: object properties: + owner: + type: string state: $ref: "#/components/schemas/RegisteredModelState" ModelVersion: diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index 8ad9b88e..ee2aa94e 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "model-registry" -version = "0.1.2" +version = "0.2.1a1" description = "Client for Kubeflow Model Registry" authors = ["Isabella Basso do Amaral "] license = "Apache-2.0" diff --git a/clients/python/src/model_registry/types/contexts.py b/clients/python/src/model_registry/types/contexts.py index 5d7d964d..04fbd061 100644 --- a/clients/python/src/model_registry/types/contexts.py +++ b/clients/python/src/model_registry/types/contexts.py @@ -129,11 +129,22 @@ class RegisteredModel(BaseContext): Attributes: name: Registered model name. + owner: Owner of this Registered Model. description: Description of the object. external_id: Customizable ID. Has to be unique among instances of the same type. """ name: str + owner: str = None + + @override + def map(self, type_id: int) -> Context: + mlmd_obj = super().map(type_id) + props = { + "owner": self.owner + } + self._map_props(props, mlmd_obj.properties) + return mlmd_obj @classmethod @override @@ -142,4 +153,5 @@ def unmap(cls, mlmd_obj: Context) -> RegisteredModel: assert isinstance( py_obj, RegisteredModel ), f"Expected RegisteredModel, got {type(py_obj)}" + py_obj.owner = mlmd_obj.properties["owner"].string_value return py_obj diff --git a/clients/python/tests/conftest.py b/clients/python/tests/conftest.py index 04e81fe4..8abb31a7 100644 --- a/clients/python/tests/conftest.py +++ b/clients/python/tests/conftest.py @@ -89,7 +89,9 @@ def teardown(): request.addfinalizer(teardown) - return MLMDStore(mlmd_conn) + to_return = MLMDStore(mlmd_conn) + sanity_check_mlmd_connection_to_db(to_return) + return to_return def set_type_attrs(mlmd_obj: ProtoTypeType, name: str, props: list[str]): @@ -99,6 +101,26 @@ def set_type_attrs(mlmd_obj: ProtoTypeType, name: str, props: list[str]): return mlmd_obj +def sanity_check_mlmd_connection_to_db(overview: MLMDStore): + # sanity check before each test: connect to MLMD directly, and dry-run any of the gRPC (read) operations; + # on newer Podman might delay in recognising volume mount files for sqlite3 db, + # hence in case of error "Cannot connect sqlite3 database: unable to open database file" make some retries. + retry_count = 0 + while retry_count < 3: + retry_count += 1 + try: + overview._mlmd_store.get_artifact_types() + return + except Exception as e: + if str(e) == "Cannot connect sqlite3 database: unable to open database file": + time.sleep(1) + else: + msg = "Failed to sanity check before each test, another type of error detected." + raise RuntimeError(msg) from e + msg = "Failed to sanity check before each test." + raise RuntimeError(msg) + + @pytest.fixture() def store_wrapper(plain_wrapper: MLMDStore) -> MLMDStore: ma_type = set_type_attrs( diff --git a/clients/python/tests/types/test_context_mapping.py b/clients/python/tests/types/test_context_mapping.py index 4ca24cff..e430fa96 100644 --- a/clients/python/tests/types/test_context_mapping.py +++ b/clients/python/tests/types/test_context_mapping.py @@ -6,7 +6,7 @@ import pytest from ml_metadata.proto import Context -from model_registry.types import ContextState, ModelVersion +from model_registry.types import ContextState, ModelVersion, RegisteredModel from .. import Mapped @@ -95,3 +95,40 @@ def test_full_model_version_unmapping(full_model_version: Mapped): assert unmapped_version.author == py_version.author assert unmapped_version.state == py_version.state assert unmapped_version.metadata == py_version.metadata + + +@pytest.fixture() +def minimal_registered_model() -> Mapped: + proto_version = Context( + name="mnist", + type_id=1, + external_id="test_external_id") + proto_version.properties["description"].string_value = "test description" + proto_version.properties["state"].string_value = "LIVE" + proto_version.properties["owner"].string_value = "my owner" + + py_regmodel = RegisteredModel(name="mnist", + owner="my owner", + external_id="test_external_id", + description="test description") + return Mapped(proto_version, py_regmodel) + + +def test_minimal_registered_model_mapping(minimal_registered_model: Mapped): + mapped_version = minimal_registered_model.py.map(1) + proto_version = minimal_registered_model.proto + assert mapped_version.name == proto_version.name + assert mapped_version.type_id == proto_version.type_id + assert mapped_version.external_id == proto_version.external_id + assert mapped_version.properties == proto_version.properties + assert mapped_version.custom_properties == proto_version.custom_properties + + +def test_minimal_registered_model_unmapping(minimal_registered_model: Mapped): + unmapped_regmodel = RegisteredModel.unmap(minimal_registered_model.proto) + py_regmodel = minimal_registered_model.py + assert unmapped_regmodel.name == py_regmodel.name + assert unmapped_regmodel.owner == py_regmodel.owner + assert unmapped_regmodel.description == py_regmodel.description + assert unmapped_regmodel.external_id == py_regmodel.external_id + assert unmapped_regmodel.state == py_regmodel.state diff --git a/docs/logical_model.md b/docs/logical_model.md index bd01b2c3..9b817329 100644 --- a/docs/logical_model.md +++ b/docs/logical_model.md @@ -442,10 +442,12 @@ This diagram summarizes the relationship between the entities: classDiagram class RegisteredModel{ +String name + +String owner +Map customProperties } class ModelVersion{ +String name + +String author +Map customProperties } class Artifact{ diff --git a/internal/converter/generated/mlmd_openapi_converter.gen.go b/internal/converter/generated/mlmd_openapi_converter.gen.go index 00663e4c..06331fea 100755 --- a/internal/converter/generated/mlmd_openapi_converter.gen.go +++ b/internal/converter/generated/mlmd_openapi_converter.gen.go @@ -173,6 +173,7 @@ func (c *MLMDToOpenAPIConverterImpl) ConvertRegisteredModel(source *proto.Contex openapiRegisteredModel.Id = converter.Int64ToString((*source).Id) openapiRegisteredModel.CreateTimeSinceEpoch = converter.Int64ToString((*source).CreateTimeSinceEpoch) openapiRegisteredModel.LastUpdateTimeSinceEpoch = converter.Int64ToString((*source).LastUpdateTimeSinceEpoch) + openapiRegisteredModel.Owner = converter.MapOwner((*source).Properties) openapiRegisteredModel.State = converter.MapRegisteredModelState((*source).Properties) pOpenapiRegisteredModel = &openapiRegisteredModel } diff --git a/internal/converter/generated/openapi_converter.gen.go b/internal/converter/generated/openapi_converter.gen.go index 067315c6..1cb8f6d1 100755 --- a/internal/converter/generated/openapi_converter.gen.go +++ b/internal/converter/generated/openapi_converter.gen.go @@ -379,6 +379,12 @@ func (c *OpenAPIConverterImpl) ConvertRegisteredModelCreate(source *openapi.Regi pString3 = &xstring3 } openapiRegisteredModel.Name = pString3 + var pString4 *string + if (*source).Owner != nil { + xstring4 := *(*source).Owner + pString4 = &xstring4 + } + openapiRegisteredModel.Owner = pString4 var pOpenapiRegisteredModelState *openapi.RegisteredModelState if (*source).State != nil { openapiRegisteredModelState := openapi.RegisteredModelState(*(*source).State) @@ -414,6 +420,12 @@ func (c *OpenAPIConverterImpl) ConvertRegisteredModelUpdate(source *openapi.Regi pString2 = &xstring2 } openapiRegisteredModel.ExternalId = pString2 + var pString3 *string + if (*source).Owner != nil { + xstring3 := *(*source).Owner + pString3 = &xstring3 + } + openapiRegisteredModel.Owner = pString3 var pOpenapiRegisteredModelState *openapi.RegisteredModelState if (*source).State != nil { openapiRegisteredModelState := openapi.RegisteredModelState(*(*source).State) diff --git a/internal/converter/mlmd_converter_util_test.go b/internal/converter/mlmd_converter_util_test.go index fb40ea1e..86ccca2e 100644 --- a/internal/converter/mlmd_converter_util_test.go +++ b/internal/converter/mlmd_converter_util_test.go @@ -414,6 +414,20 @@ func TestMapDescription(t *testing.T) { assertion.Equal("my-description", *extracted) } +func TestMapOwner(t *testing.T) { + assertion := setup(t) + + extracted := MapOwner(map[string]*proto.Value{ + "owner": { + Value: &proto.Value_StringValue{ + StringValue: "my-owner", + }, + }, + }) + + assertion.Equal("my-owner", *extracted) +} + func TestPropertyRuntime(t *testing.T) { assertion := setup(t) diff --git a/internal/converter/mlmd_openapi_converter.go b/internal/converter/mlmd_openapi_converter.go index dcd55649..28f1dedc 100644 --- a/internal/converter/mlmd_openapi_converter.go +++ b/internal/converter/mlmd_openapi_converter.go @@ -15,6 +15,7 @@ import ( // goverter:extend MapMLMDCustomProperties type MLMDToOpenAPIConverter interface { // goverter:map Properties Description | MapDescription + // goverter:map Properties Owner | MapOwner // goverter:map Properties State | MapRegisteredModelState ConvertRegisteredModel(source *proto.Context) (*openapi.RegisteredModel, error) diff --git a/internal/converter/mlmd_openapi_converter_util.go b/internal/converter/mlmd_openapi_converter_util.go index 99a9ba5e..b0094469 100644 --- a/internal/converter/mlmd_openapi_converter_util.go +++ b/internal/converter/mlmd_openapi_converter_util.go @@ -211,6 +211,10 @@ func MapDescription(properties map[string]*proto.Value) *string { return MapStringProperty(properties, "description") } +func MapOwner(properties map[string]*proto.Value) *string { + return MapStringProperty(properties, "owner") +} + func MapModelArtifactFormatName(properties map[string]*proto.Value) *string { return MapStringProperty(properties, "model_format_name") } diff --git a/internal/converter/openapi_converter.go b/internal/converter/openapi_converter.go index e5204bc0..cbd17d0f 100644 --- a/internal/converter/openapi_converter.go +++ b/internal/converter/openapi_converter.go @@ -52,7 +52,7 @@ type OpenAPIConverter interface { // Ignore all fields that ARE editable // goverter:default InitRegisteredModelWithUpdate // goverter:autoMap Existing - // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalId CustomProperties State + // goverter:ignore Id CreateTimeSinceEpoch LastUpdateTimeSinceEpoch Description ExternalId CustomProperties State Owner OverrideNotEditableForRegisteredModel(source OpenapiUpdateWrapper[openapi.RegisteredModel]) (openapi.RegisteredModel, error) // Ignore all fields that ARE editable diff --git a/internal/converter/openapi_mlmd_converter_util.go b/internal/converter/openapi_mlmd_converter_util.go index 26a7a626..ede00f3f 100644 --- a/internal/converter/openapi_mlmd_converter_util.go +++ b/internal/converter/openapi_mlmd_converter_util.go @@ -123,6 +123,14 @@ func PrefixWhenOwned(ownerId *string, entityName string) string { func MapRegisteredModelProperties(source *openapi.RegisteredModel) (map[string]*proto.Value, error) { props := make(map[string]*proto.Value) if source != nil { + if source.Owner != nil { + props["owner"] = &proto.Value{ + Value: &proto.Value_StringValue{ + StringValue: *source.Owner, + }, + } + } + if source.Description != nil { props["description"] = &proto.Value{ Value: &proto.Value_StringValue{ diff --git a/internal/mlmdtypes/mlmdtypes.go b/internal/mlmdtypes/mlmdtypes.go index 73b40399..7a169224 100644 --- a/internal/mlmdtypes/mlmdtypes.go +++ b/internal/mlmdtypes/mlmdtypes.go @@ -44,6 +44,7 @@ func CreateMLMDTypes(cc grpc.ClientConnInterface, nameConfig MLMDTypeNamesConfig Name: &nameConfig.RegisteredModelTypeName, Properties: map[string]proto.PropertyType{ "description": proto.PropertyType_STRING, + "owner": proto.PropertyType_STRING, "state": proto.PropertyType_STRING, }, }, diff --git a/internal/testutils/test_container_utils.go b/internal/testutils/test_container_utils.go index 128435a3..e99f5299 100644 --- a/internal/testutils/test_container_utils.go +++ b/internal/testutils/test_container_utils.go @@ -1,10 +1,13 @@ package testutils import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "os" + "os/exec" "testing" "github.com/kubeflow/model-registry/internal/ml_metadata/proto" @@ -15,7 +18,7 @@ import ( ) const ( - useProvider = testcontainers.ProviderDefault // or explicit to testcontainers.ProviderPodman if needed + defaultProvider = testcontainers.ProviderDefault // or explicit to testcontainers.ProviderPodman if needed mlmdImage = "gcr.io/tfx-oss-public/ml_metadata_store_server:1.14.0" sqliteFile = "metadata.sqlite.db" testConfigFolder = "test/config/ml-metadata" @@ -93,6 +96,13 @@ func SetupMLMetadataTestContainer(t *testing.T) (*grpc.ClientConn, proto.Metadat WaitingFor: wait.ForLog("Server listening on"), } + useProvider := defaultProvider + if useProvider == testcontainers.ProviderDefault { // user did not override + if tryDetectPodmanRunning() { + t.Log("Podman running detected! Will instruct TestContainers to use Podman explicitly.") // see https://github.com/testcontainers/testcontainers-go/issues/2264#issuecomment-2062092012 + useProvider = testcontainers.ProviderPodman + } + } mlmdgrpc, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: useProvider, ContainerRequest: req, @@ -137,3 +147,26 @@ func SetupMLMetadataTestContainer(t *testing.T) (*grpc.ClientConn, proto.Metadat } } } + +// simple utility function with heuristic to detect if Podman is running +func tryDetectPodmanRunning() bool { + cmd := exec.Command("podman", "machine", "info", "--format", "json") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return false + } + output := out.Bytes() + type MachineInfo struct { + Host struct { + MachineState string `json:"MachineState"` + } `json:"Host"` + } + var info MachineInfo + err = json.Unmarshal(output, &info) + if err != nil { + return false + } + return info.Host.MachineState == "Running" +} diff --git a/manifests/kustomize/OWNERS b/manifests/kustomize/OWNERS new file mode 100644 index 00000000..ea99ec7a --- /dev/null +++ b/manifests/kustomize/OWNERS @@ -0,0 +1,6 @@ +approvers: + - tarilabs + - rareddy +reviewers: + - tarilabs + - rareddy diff --git a/manifests/kustomize/README.md b/manifests/kustomize/README.md new file mode 100644 index 00000000..2c6ec9d7 --- /dev/null +++ b/manifests/kustomize/README.md @@ -0,0 +1,56 @@ +# Install Kubeflow Model Registry + +This folder contains [Kubeflow Model Registry](https://www.kubeflow.org/docs/components/model-registry/installation/) Kustomize manifests + +## Installation + +To install Kubeflow Model Registry, follow [Kubeflow Model Registry deployment documentation](https://www.kubeflow.org/docs/components/model-registry/installation/) + +The following instructions will summarize how to deploy Model Registry as separate component in the context of a default Kubeflow >=1.8 installation. + +```bash +kubectl apply -k overlays/db +``` + +As the default Kubeflow installation provides an Istio mesh, apply the necessary manifests: + +```bash +kubectl apply -k options/istio +``` + +Check everything is up and running: + +```bash +kubectl wait --for=condition=available -n kubeflow deployment/model-registry-deployment --timeout=2m +kubectl logs -n kubeflow deployment/model-registry-deployment +``` + +Optionally, you can also port-forward the REST API container port of Model Registry to interact with it from your terminal: + +```bash +kubectl port-forward svc/model-registry-service -n kubeflow 8081:8080 +``` + +And then, from another terminal: + +```bash +curl -sX 'GET' \ + 'http://localhost:8081/api/model_registry/v1alpha3/registered_models?pageSize=100&orderBy=ID&sortOrder=DESC' \ + -H 'accept: application/json' | jq +``` + +## Usage + +For a basic usage of the Kubeflow Model Registry, follow the [Kubeflow Model Registry getting started documentation](https://www.kubeflow.org/docs/components/model-registry/getting-started/) + +## Uninstall + +To uninstall the Kubeflow Model Registry run: + +```bash +# Delete istio options +kubectl delete -k options/istio + +# Delete model registry db and deployment +kubectl delete -k overlays/db +``` \ No newline at end of file diff --git a/pkg/core/core_test.go b/pkg/core/core_test.go index 71a9770a..61a38a70 100644 --- a/pkg/core/core_test.go +++ b/pkg/core/core_test.go @@ -3,7 +3,9 @@ package core import ( "context" "fmt" + "strings" "testing" + "time" "github.com/kubeflow/model-registry/internal/apiutils" "github.com/kubeflow/model-registry/internal/converter" @@ -26,8 +28,9 @@ var ( // registered models modelName string modelDescription string + modelOwner string modelExternalId string - owner string + myCustomProp string // model version modelVersionName string modelVersionDescription string @@ -86,8 +89,9 @@ func (suite *CoreTestSuite) SetupTest() { customString = "this is a customString value" modelName = "MyAwesomeModel" modelDescription = "reg model description" + modelOwner = "reg model owner" modelExternalId = "org.myawesomemodel" - owner = "owner" + myCustomProp = "myCustomPropValue" modelVersionName = "v1" modelVersionDescription = "model version description" versionExternalId = "org.myawesomemodel@v1" @@ -102,6 +106,22 @@ func (suite *CoreTestSuite) SetupTest() { entityExternalId2 = "entityExternalID2" entityDescription = "lorem ipsum entity description" executionState = "RUNNING" + + // sanity check before each test: connect to MLMD directly, and dry-run any of the gRPC (read) operations; + // on newer Podman might delay in recognising volume mount files for sqlite3 db, + // hence in case of error "Cannot connect sqlite3 database: unable to open database file" make some retries. + maxRetries := 3 + var err error + for attempt := 0; attempt < maxRetries; attempt++ { + _, err = suite.mlmdClient.GetContextTypes(context.Background(), &proto.GetContextTypesRequest{}) + if err == nil { + break + } else if !strings.Contains(err.Error(), "Cannot connect sqlite3 database: unable to open database file") { + break // err is different than expected + } + time.Sleep(1 * time.Second) + } + suite.Nilf(err, "error connecting to MLMD and dry-run any of the gRPC operations: %v", err) } // after each test @@ -131,8 +151,8 @@ func (suite *CoreTestSuite) registerModel(service api.ModelRegistryApi, override ExternalId: &modelExternalId, Description: &modelDescription, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -161,8 +181,8 @@ func (suite *CoreTestSuite) registerServingEnvironment(service api.ModelRegistry ExternalId: &eutExtID, Description: &entityDescription, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -225,8 +245,8 @@ func (suite *CoreTestSuite) registerInferenceService(service api.ModelRegistryAp RegisteredModelId: registerdModelId, ServingEnvironmentId: servingEnvironmentId, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -348,7 +368,7 @@ func (suite *CoreTestSuite) TestModelRegistryStartupWithExistingEmptyTypes() { }) suite.NotNilf(regModelResp.ContextType, "registered model type %s should exists", *registeredModelTypeName) suite.Equal(*registeredModelTypeName, *regModelResp.ContextType.Name) - suite.Equal(2, len(regModelResp.ContextType.Properties)) + suite.Equal(3, len(regModelResp.ContextType.Properties)) modelVersionResp, _ = suite.mlmdClient.GetContextType(ctx, &proto.GetContextTypeRequest{ TypeName: modelVersionTypeName, @@ -573,10 +593,11 @@ func (suite *CoreTestSuite) TestCreateRegisteredModel() { Name: &modelName, ExternalId: &modelExternalId, Description: &modelDescription, + Owner: &modelOwner, State: &state, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -600,8 +621,9 @@ func (suite *CoreTestSuite) TestCreateRegisteredModel() { suite.Equal(modelName, *ctx.Name, "saved model name should match the provided one") suite.Equal(modelExternalId, *ctx.ExternalId, "saved external id should match the provided one") suite.Equal(modelDescription, ctx.Properties["description"].GetStringValue(), "saved description should match the provided one") + suite.Equal(modelOwner, ctx.Properties["owner"].GetStringValue(), "saved owner should match the provided one") suite.Equal(string(state), ctx.Properties["state"].GetStringValue(), "saved state should match the provided one") - suite.Equal(owner, ctx.CustomProperties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(myCustomProp, ctx.CustomProperties["myCustomProp"].GetStringValue(), "saved myCustomProp custom property should match the provided one") getAllResp, err := suite.mlmdClient.GetContexts(context.Background(), &proto.GetContextsRequest{}) suite.Nilf(err, "error retrieving all contexts, not related to the test itself: %v", err) @@ -615,10 +637,11 @@ func (suite *CoreTestSuite) TestUpdateRegisteredModel() { // register a new model registeredModel := &openapi.RegisteredModel{ Name: &modelName, + Owner: &modelOwner, ExternalId: &modelExternalId, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -639,10 +662,16 @@ func (suite *CoreTestSuite) TestUpdateRegisteredModel() { // update existing model newModelExternalId := "newExternalId" newOwner := "newOwner" + newCustomProp := "updated myCustomProp" createdModel.ExternalId = &newModelExternalId + createdModel.Owner = &newOwner + (*createdModel.CustomProperties)["myCustomProp"] = openapi.MetadataValue{ + MetadataStringValue: converter.NewMetadataStringValue(newCustomProp), + } + // check can also define customProperty of name "owner", in addition to built-in property "owner" (*createdModel.CustomProperties)["owner"] = openapi.MetadataValue{ - MetadataStringValue: converter.NewMetadataStringValue(newOwner), + MetadataStringValue: converter.NewMetadataStringValue(newCustomProp), } // update the model @@ -664,7 +693,9 @@ func (suite *CoreTestSuite) TestUpdateRegisteredModel() { suite.Equal(*createdModel.Id, *ctxId, "returned model id should match the mlmd one") suite.Equal(modelName, *ctx.Name, "saved model name should match the provided one") suite.Equal(newModelExternalId, *ctx.ExternalId, "saved external id should match the provided one") - suite.Equal(newOwner, ctx.CustomProperties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(newOwner, ctx.Properties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(newCustomProp, ctx.CustomProperties["myCustomProp"].GetStringValue(), "saved myCustomProp custom property should match the provided one") + suite.Equal(newCustomProp, ctx.CustomProperties["owner"].GetStringValue(), "check can define custom property 'onwer' and should match the provided one") // update the model keeping nil name newModelExternalId = "newNewExternalId" @@ -688,7 +719,9 @@ func (suite *CoreTestSuite) TestUpdateRegisteredModel() { suite.Equal(*createdModel.Id, *ctxId, "returned model id should match the mlmd one") suite.Equal(modelName, *ctx.Name, "saved model name should match the provided one") suite.Equal(newModelExternalId, *ctx.ExternalId, "saved external id should match the provided one") - suite.Equal(newOwner, ctx.CustomProperties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(newOwner, ctx.Properties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(newCustomProp, ctx.CustomProperties["myCustomProp"].GetStringValue(), "saved myCustomProp custom property should match the provided one") + suite.Equal(newCustomProp, ctx.CustomProperties["owner"].GetStringValue(), "check can define custom property 'onwer' and should match the provided one") } func (suite *CoreTestSuite) TestGetRegisteredModelById() { @@ -702,8 +735,8 @@ func (suite *CoreTestSuite) TestGetRegisteredModelById() { ExternalId: &modelExternalId, State: &state, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -970,7 +1003,7 @@ func (suite *CoreTestSuite) TestCreateModelVersion() { createdVersion, err := service.UpsertModelVersion(modelVersion, ®isteredModelId) suite.Nilf(err, "error creating new model version for %d", registeredModelId) - suite.Equal((*createdVersion).RegisteredModelId, registeredModelId, "RegisteredModelId should match the actual owner") + suite.Equal((*createdVersion).RegisteredModelId, registeredModelId, "RegisteredModelId should match the actual owner-entity") suite.NotNilf(createdVersion.Id, "created model version should not have nil Id") @@ -1046,7 +1079,7 @@ func (suite *CoreTestSuite) TestUpdateModelVersion() { updatedVersion, err := service.UpsertModelVersion(createdVersion, ®isteredModelId) suite.Nilf(err, "error updating new model version for %s: %v", registeredModelId, err) - suite.Equal((*updatedVersion).RegisteredModelId, registeredModelId, "RegisteredModelId should match the actual owner") + suite.Equal((*updatedVersion).RegisteredModelId, registeredModelId, "RegisteredModelId should match the actual owner-entity") updateVersionId, _ := converter.StringToInt64(updatedVersion.Id) suite.Equal(*createdVersionId, *updateVersionId, "created and updated model version should have same id") @@ -1936,8 +1969,8 @@ func (suite *CoreTestSuite) TestCreateServingEnvironment() { ExternalId: &entityExternalId, Description: &entityDescription, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -1961,7 +1994,7 @@ func (suite *CoreTestSuite) TestCreateServingEnvironment() { suite.Equal(entityName, *ctx.Name, "saved name should match the provided one") suite.Equal(entityExternalId, *ctx.ExternalId, "saved external id should match the provided one") suite.Equal(entityDescription, ctx.Properties["description"].GetStringValue(), "saved description should match the provided one") - suite.Equal(owner, ctx.CustomProperties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(myCustomProp, ctx.CustomProperties["myCustomProp"].GetStringValue(), "saved myCustomProp custom property should match the provided one") getAllResp, err := suite.mlmdClient.GetContexts(context.Background(), &proto.GetContextsRequest{}) suite.Nilf(err, "error retrieving all contexts, not related to the test itself: %v", err) @@ -1977,8 +2010,8 @@ func (suite *CoreTestSuite) TestUpdateServingEnvironment() { Name: &entityName, ExternalId: &entityExternalId, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } @@ -1998,11 +2031,11 @@ func (suite *CoreTestSuite) TestUpdateServingEnvironment() { // update existing entity newExternalId := "newExternalId" - newOwner := "newOwner" + newCustomProp := "newCustomProp" createdEntity.ExternalId = &newExternalId - (*createdEntity.CustomProperties)["owner"] = openapi.MetadataValue{ - MetadataStringValue: converter.NewMetadataStringValue(newOwner), + (*createdEntity.CustomProperties)["myCustomProp"] = openapi.MetadataValue{ + MetadataStringValue: converter.NewMetadataStringValue(newCustomProp), } // update the entity @@ -2024,7 +2057,7 @@ func (suite *CoreTestSuite) TestUpdateServingEnvironment() { suite.Equal(*createdEntity.Id, *ctxId, "returned entity id should match the mlmd one") suite.Equal(entityName, *ctx.Name, "saved entity name should match the provided one") suite.Equal(newExternalId, *ctx.ExternalId, "saved external id should match the provided one") - suite.Equal(newOwner, ctx.CustomProperties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(newCustomProp, ctx.CustomProperties["myCustomProp"].GetStringValue(), "saved myCustomProp custom property should match the provided one") // update the entity under test, keeping nil name newExternalId = "newNewExternalId" @@ -2048,7 +2081,7 @@ func (suite *CoreTestSuite) TestUpdateServingEnvironment() { suite.Equal(*createdEntity.Id, *ctxId, "returned entity id should match the mlmd one") suite.Equal(entityName, *ctx.Name, "saved entity name should match the provided one") suite.Equal(newExternalId, *ctx.ExternalId, "saved external id should match the provided one") - suite.Equal(newOwner, ctx.CustomProperties["owner"].GetStringValue(), "saved owner custom property should match the provided one") + suite.Equal(newCustomProp, ctx.CustomProperties["myCustomProp"].GetStringValue(), "saved myCustomProp custom property should match the provided one") } func (suite *CoreTestSuite) TestGetServingEnvironmentById() { @@ -2060,8 +2093,8 @@ func (suite *CoreTestSuite) TestGetServingEnvironmentById() { Name: &entityName, ExternalId: &entityExternalId, CustomProperties: &map[string]openapi.MetadataValue{ - "owner": { - MetadataStringValue: converter.NewMetadataStringValue(owner), + "myCustomProp": { + MetadataStringValue: converter.NewMetadataStringValue(myCustomProp), }, }, } diff --git a/pkg/openapi/model_registered_model.go b/pkg/openapi/model_registered_model.go index 4dd6dd94..73ce09a6 100644 --- a/pkg/openapi/model_registered_model.go +++ b/pkg/openapi/model_registered_model.go @@ -33,6 +33,7 @@ type RegisteredModel struct { CreateTimeSinceEpoch *string `json:"createTimeSinceEpoch,omitempty"` // Output only. Last update time of the resource since epoch in millisecond since epoch. LastUpdateTimeSinceEpoch *string `json:"lastUpdateTimeSinceEpoch,omitempty"` + Owner *string `json:"owner,omitempty"` State *RegisteredModelState `json:"state,omitempty"` } @@ -281,6 +282,38 @@ func (o *RegisteredModel) SetLastUpdateTimeSinceEpoch(v string) { o.LastUpdateTimeSinceEpoch = &v } +// GetOwner returns the Owner field value if set, zero value otherwise. +func (o *RegisteredModel) GetOwner() string { + if o == nil || IsNil(o.Owner) { + var ret string + return ret + } + return *o.Owner +} + +// GetOwnerOk returns a tuple with the Owner field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegisteredModel) GetOwnerOk() (*string, bool) { + if o == nil || IsNil(o.Owner) { + return nil, false + } + return o.Owner, true +} + +// HasOwner returns a boolean if a field has been set. +func (o *RegisteredModel) HasOwner() bool { + if o != nil && !IsNil(o.Owner) { + return true + } + + return false +} + +// SetOwner gets a reference to the given string and assigns it to the Owner field. +func (o *RegisteredModel) SetOwner(v string) { + o.Owner = &v +} + // GetState returns the State field value if set, zero value otherwise. func (o *RegisteredModel) GetState() RegisteredModelState { if o == nil || IsNil(o.State) { @@ -344,6 +377,9 @@ func (o RegisteredModel) ToMap() (map[string]interface{}, error) { if !IsNil(o.LastUpdateTimeSinceEpoch) { toSerialize["lastUpdateTimeSinceEpoch"] = o.LastUpdateTimeSinceEpoch } + if !IsNil(o.Owner) { + toSerialize["owner"] = o.Owner + } if !IsNil(o.State) { toSerialize["state"] = o.State } diff --git a/pkg/openapi/model_registered_model_create.go b/pkg/openapi/model_registered_model_create.go index 97f25c9d..cf6df186 100644 --- a/pkg/openapi/model_registered_model_create.go +++ b/pkg/openapi/model_registered_model_create.go @@ -27,6 +27,7 @@ type RegisteredModelCreate struct { ExternalId *string `json:"externalId,omitempty"` // The client provided name of the artifact. This field is optional. If set, it must be unique among all the artifacts of the same artifact type within a database instance and cannot be changed once set. Name *string `json:"name,omitempty"` + Owner *string `json:"owner,omitempty"` State *RegisteredModelState `json:"state,omitempty"` } @@ -179,6 +180,38 @@ func (o *RegisteredModelCreate) SetName(v string) { o.Name = &v } +// GetOwner returns the Owner field value if set, zero value otherwise. +func (o *RegisteredModelCreate) GetOwner() string { + if o == nil || IsNil(o.Owner) { + var ret string + return ret + } + return *o.Owner +} + +// GetOwnerOk returns a tuple with the Owner field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegisteredModelCreate) GetOwnerOk() (*string, bool) { + if o == nil || IsNil(o.Owner) { + return nil, false + } + return o.Owner, true +} + +// HasOwner returns a boolean if a field has been set. +func (o *RegisteredModelCreate) HasOwner() bool { + if o != nil && !IsNil(o.Owner) { + return true + } + + return false +} + +// SetOwner gets a reference to the given string and assigns it to the Owner field. +func (o *RegisteredModelCreate) SetOwner(v string) { + o.Owner = &v +} + // GetState returns the State field value if set, zero value otherwise. func (o *RegisteredModelCreate) GetState() RegisteredModelState { if o == nil || IsNil(o.State) { @@ -233,6 +266,9 @@ func (o RegisteredModelCreate) ToMap() (map[string]interface{}, error) { if !IsNil(o.Name) { toSerialize["name"] = o.Name } + if !IsNil(o.Owner) { + toSerialize["owner"] = o.Owner + } if !IsNil(o.State) { toSerialize["state"] = o.State } diff --git a/pkg/openapi/model_registered_model_update.go b/pkg/openapi/model_registered_model_update.go index add24cee..fe4b3404 100644 --- a/pkg/openapi/model_registered_model_update.go +++ b/pkg/openapi/model_registered_model_update.go @@ -25,6 +25,7 @@ type RegisteredModelUpdate struct { Description *string `json:"description,omitempty"` // The external id that come from the clients’ system. This field is optional. If set, it must be unique among all resources within a database instance. ExternalId *string `json:"externalId,omitempty"` + Owner *string `json:"owner,omitempty"` State *RegisteredModelState `json:"state,omitempty"` } @@ -145,6 +146,38 @@ func (o *RegisteredModelUpdate) SetExternalId(v string) { o.ExternalId = &v } +// GetOwner returns the Owner field value if set, zero value otherwise. +func (o *RegisteredModelUpdate) GetOwner() string { + if o == nil || IsNil(o.Owner) { + var ret string + return ret + } + return *o.Owner +} + +// GetOwnerOk returns a tuple with the Owner field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RegisteredModelUpdate) GetOwnerOk() (*string, bool) { + if o == nil || IsNil(o.Owner) { + return nil, false + } + return o.Owner, true +} + +// HasOwner returns a boolean if a field has been set. +func (o *RegisteredModelUpdate) HasOwner() bool { + if o != nil && !IsNil(o.Owner) { + return true + } + + return false +} + +// SetOwner gets a reference to the given string and assigns it to the Owner field. +func (o *RegisteredModelUpdate) SetOwner(v string) { + o.Owner = &v +} + // GetState returns the State field value if set, zero value otherwise. func (o *RegisteredModelUpdate) GetState() RegisteredModelState { if o == nil || IsNil(o.State) { @@ -196,6 +229,9 @@ func (o RegisteredModelUpdate) ToMap() (map[string]interface{}, error) { if !IsNil(o.ExternalId) { toSerialize["externalId"] = o.ExternalId } + if !IsNil(o.Owner) { + toSerialize["owner"] = o.Owner + } if !IsNil(o.State) { toSerialize["state"] = o.State } diff --git a/test/robot/UserStory.robot b/test/robot/UserStory.robot index 0acf173f..8121bf06 100644 --- a/test/robot/UserStory.robot +++ b/test/robot/UserStory.robot @@ -80,3 +80,10 @@ As a MLOps engineer I would like to store some labels ${r} Then I get ModelArtifactByID id=${aId} And Should be equal ${r["description"]} sed do eiusmod tempor incididunt And Dictionaries Should Be Equal ${r["customProperties"]} ${cp3} + +As a MLOps engineer I would like to store an owner for the RegisteredModel + Set To Dictionary ${registered_model} description=Lorem ipsum dolor sit amet name=${name} owner=My owner + ${rId} Given I create a RegisteredModel payload=${registered_model} + ${r} Then I get RegisteredModelByID id=${rId} + And Should be equal ${r["description"]} Lorem ipsum dolor sit amet + And Should be equal ${r["owner"]} My owner