From b3d2259ea846c264b6c53d3efddf5bb91edec8a2 Mon Sep 17 00:00:00 2001 From: Ahmed Abdalla Date: Fri, 22 Mar 2024 02:56:07 +0100 Subject: [PATCH] Refactor the models command to use the edge client Using the introduced edge client, refactor the model command to simply use the edge client for listing and adding models. Also refactor the commands design to streamline the flags and commands initialization. --- Makefile | 1 + cli/examples/params.yaml.sample | 47 ++++++++ cli/go.mod | 20 +--- cli/go.sum | 59 +--------- cli/pkg/commands/common/cmd.go | 106 ++++++++++++++++++ cli/pkg/commands/common/errors.go | 25 +++++ cli/pkg/commands/common/styles.go | 35 ++++++ cli/pkg/commands/flags/flags.go | 105 ++++++++++++++++++ cli/pkg/commands/models.go | 163 --------------------------- cli/pkg/commands/models/add.go | 36 ++++++ cli/pkg/commands/models/models.go | 176 ++++++++++++++++++++++++++++++ cli/pkg/commands/root.go | 26 ++--- cli/pkg/modelregistry/client.go | 82 +++++++++++++- cli/pkg/modelregistry/errors.go | 5 + cli/pkg/pipelines/params.go | 74 +++++++++++++ docs/cli.md | 135 +++++++++++++++++++++++ 16 files changed, 841 insertions(+), 254 deletions(-) create mode 100644 cli/examples/params.yaml.sample create mode 100644 cli/pkg/commands/common/cmd.go create mode 100644 cli/pkg/commands/common/errors.go create mode 100644 cli/pkg/commands/common/styles.go create mode 100644 cli/pkg/commands/flags/flags.go delete mode 100644 cli/pkg/commands/models.go create mode 100644 cli/pkg/commands/models/add.go create mode 100644 cli/pkg/commands/models/models.go create mode 100644 cli/pkg/pipelines/params.go create mode 100644 docs/cli.md diff --git a/Makefile b/Makefile index 52122ade..a88efafa 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,7 @@ endif (cd test/e2e-tests/tests && S3_BUCKET=${S3_BUCKET} TARGET_IMAGE_TAGS_JSON=${TARGET_IMAGE_TAGS_JSON} NAMESPACE=${NAMESPACE} ${GO} test -timeout 30m) test: + ${MAKE} -C cli cli-test @(./test/shell-pipeline-tests/seldon-bike-rentals/pipelines-test-seldon-bike-rentals.sh) @(./test/shell-pipeline-tests/openvino-tensorflow-housing/pipelines-test-openvino-tensorflow-housing.sh) diff --git a/cli/examples/params.yaml.sample b/cli/examples/params.yaml.sample new file mode 100644 index 00000000..823923ad --- /dev/null +++ b/cli/examples/params.yaml.sample @@ -0,0 +1,47 @@ +params: + # The name of the S3 bucket where the model is stored + - name: s3-bucket-name + value: BUCKET_NAME + # The URL of the git repository where the containerfile is stored + - name: git-containerfile-repo + value: GIT_REPO_URL + # The branch of the git repository where the containerfile is stored + - name: git-containerfile-revision + value: GIT_BRANCH + # The relative path to the containerfile in the git repository + - name: containerfileRelativePath + value: RELATIVE_PATH + # The method used to fetch the model (s3 or git) + - name: fetch-model + value: FETCH_METHOD + # The URL of the git repository where the model is stored. This is only used if fetch-model is set to git. + - name: git-model-repo + value: GIT_REPO_URL + # The relative path to the model in the git repository. This is only used if fetch-model is set to git. + - name: modelRelativePath + value: RELATIVE_PATH + # The branch of the git repository where the model is stored. This is only used if fetch-model is set to git. + - name: git-model-revision + value: GIT_BRANCH + # The name of the model serving test endpoint (e.g. invocations) + - name: test-endpoint + value: ENDPOINT_NAME + # The candidate image tag reference. This is the intermediate image that is built during the pipeline. + # e.g. image-registry.openshift-image-registry.svc:5000/$(context.pipelineRun.namespace)/$(params.model-name):$(params.model-version)-candidate + - name: candidate-image-tag-reference + value: CANDIDATE_IMAGE_TAG + # The target image tag references. These are the final images that are pushed to the image registry. A typical value would be: + # - quay.io/rhoai-edge/$(params.model-name):$(params.model-version)-$(context.pipelineRun.uid) + # - quay.io/rhoai-edge/$(params.model-name):$(params.model-version) + # - quay.io/rhoai-edge/$(params.model-name):latest + - name: target-image-tag-references + value: + - TARGET_IMAGE_TAG_1 + - TARGET_IMAGE_TAG_2 + # The action to take upon the completion of the pipeline (e.g. delete) + - name: upon-end + value: ACTION + # The name of the secret that contains the S3 credentials + - name: s3SecretName + value: SECRET_NAME + # The name of the config map that contains the test data diff --git a/cli/go.mod b/cli/go.mod index e959966e..e4e6de93 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -8,44 +8,28 @@ require ( github.com/charmbracelet/lipgloss v0.9.1 github.com/kubeflow/model-registry v0.0.0-20240312073310-67d9e4deff70 github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 + gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/rivo/uniseg v0.4.6 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index 837284fb..91166d23 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -9,18 +9,7 @@ github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -34,8 +23,6 @@ github.com/kubeflow/model-registry v0.0.0-20240312073310-67d9e4deff70 h1:pQ8qHl3 github.com/kubeflow/model-registry v0.0.0-20240312073310-67d9e4deff70/go.mod h1:3EgC6tpQm8sIgftEGujni7lZDx50/GjI1wLVpOOxw5I= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -43,8 +30,6 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -53,51 +38,19 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -111,8 +64,6 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/pkg/commands/common/cmd.go b/cli/pkg/commands/common/cmd.go new file mode 100644 index 00000000..576fd830 --- /dev/null +++ b/cli/pkg/commands/common/cmd.go @@ -0,0 +1,106 @@ +/* +Copyright 2024. Open Data Hub 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 common + +import ( + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/opendatahub-io/ai-edge/cli/pkg/commands/flags" + "github.com/spf13/cobra" +) + +// SubCommand is a type to represent the subcommand +type SubCommand int + +const ( + // SubCommandList is a subcommand to list items + SubCommandList SubCommand = iota + // SubCommandAdd is a subcommand to add items + SubCommandAdd +) + +// NewCmd creates a new cobra command. +// +// The command will create a new tea program, passing the model created by the modelFactory, and run it. +// The modelFactory will be called with the args, flags and subCommand. +// +// Example: +// cmd := NewCmd( +// "images", +// "List images", +// `List images`, +// cobra.ExactArgs(3), +// []flags.Flag{flags.FlagModelRegistryUrl}, +// SubCommandList, +// func(args []string, flags map[string]string, subCommand SubCommand) tea.Model { +// return NewImagesModel(args, flags, subCommand) +// }, +// ) +func NewCmd( + use, short, long string, + args cobra.PositionalArgs, + flags []flags.Flag, + command SubCommand, + modelFactory func(args []string, flags map[string]string, subCommand SubCommand) tea.Model, +) *cobra.Command { + + cmd := cobra.Command{ + Use: use, + Short: short, + Long: long, + Args: args, + Run: func(cmd *cobra.Command, args []string) { + ff := make(map[string]string) + for _, f := range flags { + v := "" + err := error(nil) + if f.IsInherited() { + v, err = cmd.InheritedFlags().GetString(f.String()) + if err != nil { + cmd.PrintErrf("Error reading inherited flag %s: %v\n", f, err) + os.Exit(1) + } + } else { + v, err = cmd.Flags().GetString(f.String()) + if err != nil { + cmd.PrintErrf("Error reading flag %s: %v\n", f, err) + os.Exit(1) + } + } + ff[f.String()] = v + } + _, err := tea.NewProgram(modelFactory(args, ff, command)).Run() + if err != nil { + cmd.PrintErrf("Error: %v\n", err) + os.Exit(1) + } + }, + } + + for _, f := range flags { + if !f.IsParentFlag() { + if f.IsInherited() { + cmd.PersistentFlags().StringP(f.String(), f.Shorthand(), f.Value(), f.Usage()) + } else { + cmd.Flags().StringP(f.String(), f.Shorthand(), f.Value(), f.Usage()) + } + } + } + + return &cmd +} diff --git a/cli/pkg/commands/common/errors.go b/cli/pkg/commands/common/errors.go new file mode 100644 index 00000000..563268fd --- /dev/null +++ b/cli/pkg/commands/common/errors.go @@ -0,0 +1,25 @@ +/* +Copyright 2024. Open Data Hub 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 common + +// ErrMsg is a wrapper for an error that implements the error interface. +// +// This is useful for returning an error from a model in the bubbletea program. +type ErrMsg struct{ Err error } + +// Error returns the error message. +func (e ErrMsg) Error() string { return e.Err.Error() } diff --git a/cli/pkg/commands/common/styles.go b/cli/pkg/commands/common/styles.go new file mode 100644 index 00000000..b8148297 --- /dev/null +++ b/cli/pkg/commands/common/styles.go @@ -0,0 +1,35 @@ +/* +Copyright 2024. Open Data Hub 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 common + +import "github.com/charmbracelet/lipgloss" + +// TableBaseStyle is the base style for the table +var TableBaseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#04B575")) + +// MessageStyle is the style for regular messages +var MessageStyle = lipgloss.NewStyle(). + Bold(true) + +// ErrorStyle is the style for error messages +var ErrorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FF0000")). + Bold(true). + Height(4). + Width(120) diff --git a/cli/pkg/commands/flags/flags.go b/cli/pkg/commands/flags/flags.go new file mode 100644 index 00000000..a9b5ac6d --- /dev/null +++ b/cli/pkg/commands/flags/flags.go @@ -0,0 +1,105 @@ +/* +Copyright 2024. Open Data Hub 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 flags + +import ( + "fmt" + "os" +) + +// Flag represents a command line flag. +// +// Flags can be inherited by subcommands, in which case they will be passed to the subcommand. +type Flag struct { + name string + inherited bool // Flag is inherited by subcommands + parentFlag bool // Flag is defined in the parent command + shorthand string + value string + usage string +} + +var ( + // FlagModelRegistryURL is the URL of the model registry + FlagModelRegistryURL = Flag{ + name: "model-registry-url", inherited: true, shorthand: "m", value: "http://localhost:8080", + usage: "URL of the model registry", + } + + // FlagKubeconfig is the path to the kubeconfig file + FlagKubeconfig = Flag{ + name: "kubeconfig", inherited: true, shorthand: "k", + value: fmt.Sprintf("%s/.kube/config", os.Getenv("HOME")), + usage: "path to the kubeconfig file", + } + + // FlagNamespace is the namespace to use + FlagNamespace = Flag{name: "namespace"} + + // FlagParams is the path to the build parameters file + FlagParams = Flag{ + name: "params", + shorthand: "p", + value: "params.yaml", + usage: "path to the build parameters file", + } + + // Flags is a list of all flags + Flags = []Flag{FlagKubeconfig, FlagModelRegistryURL, FlagNamespace, FlagParams} +) + +// String returns the name of the flag. +func (f Flag) String() string { + return f.name +} + +// SetInherited sets the flag to be inherited by subcommands. +func (f Flag) SetInherited() Flag { + f.inherited = true + return f +} + +// IsInherited returns true if the flag is inherited by subcommands. +func (f Flag) IsInherited() bool { + return f.inherited +} + +// SetParentFlag sets the flag as one that's defined in the parent command. +func (f Flag) SetParentFlag() Flag { + f.parentFlag = true + return f +} + +// IsParentFlag returns true if the flag is defined in the parent command. +func (f Flag) IsParentFlag() bool { + return f.parentFlag +} + +// Shorthand returns the shorthand of the flag. +func (f Flag) Shorthand() string { + return f.shorthand +} + +// Value returns the value of the flag. +func (f Flag) Value() string { + return f.value +} + +// Usage returns the usage of the flag. +func (f Flag) Usage() string { + return f.usage +} diff --git a/cli/pkg/commands/models.go b/cli/pkg/commands/models.go deleted file mode 100644 index 1ec8cf7c..00000000 --- a/cli/pkg/commands/models.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2023 KStreamer 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 commands - -import ( - "context" - "fmt" - "os" - - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - openapiclient "github.com/kubeflow/model-registry/pkg/openapi" - "github.com/spf13/cobra" -) - -var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("#04B575")) - -var errorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FF0000")). - Bold(true). - Height(4). - Width(120) - -type registeredModelsMsg *openapiclient.RegisteredModelList - -type errMsg struct{ err error } - -func (e errMsg) Error() string { return e.err.Error() } - -func listRegisteredModels() tea.Msg { - configuration := openapiclient.NewConfiguration() - configuration.Servers = openapiclient.ServerConfigurations{ - { - URL: modelRegistryURL, - }, - } - - apiClient := openapiclient.NewAPIClient(configuration) - models, httpRes, err := apiClient.ModelRegistryServiceAPI.GetRegisteredModels( - context.Background(), - ).Execute() - if err != nil { - return errMsg{err} - } - if httpRes.StatusCode != 200 { - fmt.Printf("not 200: %d\n", httpRes.StatusCode) - return errMsg{ - fmt.Errorf( - "Failed to get models, calling the model registry API returned status code %d\n", httpRes.StatusCode, - ), - } - } - return registeredModelsMsg(models) -} - -type model struct { - registeredModelsList *openapiclient.RegisteredModelList - cursor int - choices int - err error -} - -func (m model) Init() tea.Cmd { - return listRegisteredModels -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case errMsg: - m.err = msg - return m, tea.Quit - - case registeredModelsMsg: - m.registeredModelsList = msg - m.choices = len(m.registeredModelsList.Items) - return m, tea.Quit - } - return m, nil -} - -func (m model) View() string { - if m.err != nil { - return errorStyle.Render(fmt.Sprintf("Error: %s", m.err)) - // return "" - } - columns := []table.Column{ - {Title: "Id", Width: 4}, - {Title: "Name", Width: 20}, - {Title: "Description", Width: 60}, - } - - rows := make([]table.Row, 0) - - if m.registeredModelsList != nil { - for _, model := range m.registeredModelsList.Items { - rows = append( - rows, table.Row{ - model.GetId(), - model.GetName(), - model.GetDescription(), - }, - ) - } - } - - t := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithHeight(len(rows)+1), - ) - - s := table.DefaultStyles() - s.Cell.Foreground(lipgloss.Color("#FFF")) - s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("#04B575")). - BorderBottom(true). - Bold(true) - t.SetStyles(s) - t.SetCursor(m.cursor) - return baseStyle.Render(t.View()) + "\n" -} - -// modelsCmd represents the models command -var modelsCmd = &cobra.Command{ - Use: "models", - Short: "Manage models", - Long: `Manage Open Data Hub models from the command line. - -This command will list all the registered models available in the Open Data Hub model registry.`, - Run: func(cmd *cobra.Command, args []string) { - m, err := tea.NewProgram(model{}).Run() - if err != nil { - cmd.PrintErrf("Error: %v\n", err) - os.Exit(1) - } - if m.(model).err != nil { - // cmd.PrintErrf("Error: %v\n", m.(model).err) - os.Exit(1) - } - }, -} - -func init() { - rootCmd.AddCommand(modelsCmd) -} diff --git a/cli/pkg/commands/models/add.go b/cli/pkg/commands/models/add.go new file mode 100644 index 00000000..4e526fd6 --- /dev/null +++ b/cli/pkg/commands/models/add.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. Open Data Hub 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 models + +import ( + "github.com/opendatahub-io/ai-edge/cli/pkg/commands/common" + "github.com/opendatahub-io/ai-edge/cli/pkg/commands/flags" + "github.com/spf13/cobra" +) + +var addCmd = common.NewCmd( + "add ", + "Add a model and version to the model registry", + `Add a model to the model registry + +This command allows you to add a model and version to the model registry along with the build parameters from the +provided parameters file.`, + cobra.ExactArgs(3), + []flags.Flag{flags.FlagModelRegistryURL.SetParentFlag(), flags.FlagParams}, + common.SubCommandAdd, + NewTeaModel, +) diff --git a/cli/pkg/commands/models/models.go b/cli/pkg/commands/models/models.go new file mode 100644 index 00000000..45e768fa --- /dev/null +++ b/cli/pkg/commands/models/models.go @@ -0,0 +1,176 @@ +/* +Copyright 2024. Open Data Hub 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 models + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/opendatahub-io/ai-edge/cli/pkg/commands/common" + "github.com/opendatahub-io/ai-edge/cli/pkg/commands/flags" + "github.com/opendatahub-io/ai-edge/cli/pkg/edgeclient" + "github.com/opendatahub-io/ai-edge/cli/pkg/pipelines" + "github.com/spf13/cobra" +) + +type registeredModelsMsg []edgeclient.Model +type newModelAddedMsg struct{} + +type teaModel struct { + args []string + flags map[string]string + edgeClient *edgeclient.Client + registeredModelsList []edgeclient.Model + err error + subCommand common.SubCommand +} + +// NewTeaModel creates a new bubbletea model for the models command +func NewTeaModel(args []string, flgs map[string]string, subCommand common.SubCommand) tea.Model { + return &teaModel{ + args: args, + flags: flgs, + edgeClient: edgeclient.NewClient(flgs[flags.FlagModelRegistryURL.String()]), + subCommand: subCommand, + } +} + +// Init initializes the model according to the subcommand +func (m teaModel) Init() tea.Cmd { + switch m.subCommand { + case common.SubCommandList: + return m.listRegisteredModels() + case common.SubCommandAdd: + return m.addModel() + } + return nil +} + +func (m teaModel) listRegisteredModels() func() tea.Msg { + c := m.edgeClient + return func() tea.Msg { + models, err := c.GetModels() + if err != nil { + return common.ErrMsg{err} + } + return registeredModelsMsg(models) + } +} + +func (m teaModel) addModel() func() tea.Msg { + c := m.edgeClient + return func() tea.Msg { + params, err := pipelines.ReadParams(m.flags[flags.FlagParams.String()]) + if err != nil { + return common.ErrMsg{err} + } + _, err = c.AddNewModelWithImage(m.args[0], m.args[1], m.args[2], "", params.ToSimpleMap()) + if err != nil { + return common.ErrMsg{err} + } + return newModelAddedMsg{} + + } +} + +// Update updates the model according to the message +func (m teaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case common.ErrMsg: + m.err = msg + return m, tea.Quit + + case registeredModelsMsg: + m.registeredModelsList = msg + return m, tea.Quit + case newModelAddedMsg: + return m, tea.Quit + } + return m, nil +} + +// View returns the view corresponding to the subcommand +func (m teaModel) View() string { + if m.err != nil { + return common.ErrorStyle.Render(fmt.Sprintf("Error: %s", m.err)) + } + switch m.subCommand { + case common.SubCommandList: + return m.viewListModels() + case common.SubCommandAdd: + return common.MessageStyle.Render("\nModel added successfully\n\n") + + } + return "" +} + +func (m teaModel) viewListModels() string { + columns := []table.Column{ + {Title: "Id", Width: 4}, + {Title: "Name", Width: 20}, + {Title: "Description", Width: 60}, + } + + rows := make([]table.Row, 0) + + if m.registeredModelsList != nil { + for _, model := range m.registeredModelsList { + rows = append( + rows, table.Row{ + model.ID, + model.Name, + model.Description, + }, + ) + } + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithHeight(len(rows)+1), + ) + + s := table.DefaultStyles() + s.Cell.Foreground(lipgloss.Color("#FFF")) + s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#04B575")). + BorderBottom(true). + Bold(true) + t.SetStyles(s) + return common.TableBaseStyle.Render(t.View()) + "\n" +} + +// Cmd represents the models command +var Cmd = common.NewCmd( + "models", + "Manage models", + `Manage Open Data Hub models from the command line. + +This command will list all the registered models available in the Open Data Hub model registry.`, + cobra.NoArgs, + []flags.Flag{flags.FlagModelRegistryURL.SetParentFlag()}, + common.SubCommandList, + NewTeaModel, +) + +func init() { + Cmd.AddCommand(addCmd) +} diff --git a/cli/pkg/commands/root.go b/cli/pkg/commands/root.go index 39a59ff8..dc81e03e 100644 --- a/cli/pkg/commands/root.go +++ b/cli/pkg/commands/root.go @@ -17,16 +17,13 @@ limitations under the License. package commands import ( - "fmt" "os" + "github.com/opendatahub-io/ai-edge/cli/pkg/commands/flags" + "github.com/opendatahub-io/ai-edge/cli/pkg/commands/models" "github.com/spf13/cobra" - "github.com/spf13/viper" ) -var kubeconfig string -var modelRegistryURL string - // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "odh-cli", @@ -36,6 +33,11 @@ var rootCmd = &cobra.Command{ This application is a tool to perform various operations on Open Data Hub.`, } +var rootFlags = []flags.Flag{ + flags.FlagModelRegistryURL, + flags.FlagKubeconfig, +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { @@ -47,17 +49,13 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) + + for _, f := range rootFlags { + rootCmd.PersistentFlags().StringP(f.String(), f.Shorthand(), f.Value(), f.Usage()) + } + rootCmd.AddCommand(models.Cmd) } // initConfig reads in config file and ENV variables if set. func initConfig() { - viper.AutomaticEnv() // read in environment variables that match - - if kubeconfig = viper.GetString("KUBECONFIG"); kubeconfig == "" { - kubeconfig = fmt.Sprintf("%s/.kube/config", os.Getenv("HOME")) - } - - if modelRegistryURL = viper.GetString("MODEL_REGISTRY_URL"); modelRegistryURL == "" { - modelRegistryURL = "http://localhost:8080" - } } diff --git a/cli/pkg/modelregistry/client.go b/cli/pkg/modelregistry/client.go index b08a65bc..7af07b7c 100644 --- a/cli/pkg/modelregistry/client.go +++ b/cli/pkg/modelregistry/client.go @@ -21,6 +21,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "github.com/kubeflow/model-registry/pkg/openapi" @@ -67,15 +68,28 @@ func (c *Client) AutoRegisterModelVersionArtifact( if modelName == "" || modelDescription == "" || versionName == "" { return nil, nil, nil, fmt.Errorf("name, description and version are required") } - - m, err := c.CreateRegisteredModel(modelName, modelDescription, nil) + m, err := c.FindRegisteredModel(modelName) if err != nil { - return nil, nil, nil, err + if !errors.Is(err, ErrModelNotFound) { + return nil, nil, nil, err + } + // If the model is not found, create a new model + m, err = c.CreateRegisteredModel(modelName, modelDescription, nil) + if err != nil { + return nil, nil, nil, err + } } - v, err := c.CreateModelVersion(m.GetId(), versionName, metaData) + v, err := c.FindModelVersion(m.GetId(), versionName) if err != nil { - return nil, nil, nil, err + if !errors.Is(err, ErrVersionNotFound) { + return nil, nil, nil, err + } + // If the version is not found, create a new version + v, err = c.CreateModelVersion(m.GetId(), versionName, metaData) + if err != nil { + return nil, nil, nil, err + } } a, err := c.CreateModelArtifact( @@ -241,6 +255,64 @@ func (c *Client) GetRegisteredModels() ([]openapi.RegisteredModel, error) { return models.Items, nil } +// FindRegisteredModel finds a registered model by name +func (c *Client) FindRegisteredModel(name string) (*openapi.RegisteredModel, error) { + if name == "" { + return nil, fmt.Errorf("name is required") + } + model, resp, err := c.modelRegistryClient.ModelRegistryServiceAPI.FindRegisteredModel( + context.Background(), + ).Name(name).Execute() + if err != nil { + if resp == nil { + return nil, fmt.Errorf("error looking up model: %w", err) + } + if resp.StatusCode != 200 { + // TODO: Remove this workaround when model registry returns 404 when the model is not found + if isOpenAPIErrorOfKind(err, ErrModelNotFoundForName) { + return nil, fmt.Errorf("%w. model name: %s", ErrModelNotFound, name) + } + } + // This is a weird case where we got a response and an error that we're unable to handle. + return nil, fmt.Errorf( + "error while querying for a registered model: server responded with %s %w", resp.Status, err, + ) + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("failed to find a registered model: %s", resp.Status) + } + return model, nil +} + +// FindModelVersion finds a model version by name and model ID +func (c *Client) FindModelVersion(modelID, versionName string) (*openapi.ModelVersion, error) { + if modelID == "" || versionName == "" { + return nil, fmt.Errorf("model ID and version name are required") + } + version, resp, err := c.modelRegistryClient.ModelRegistryServiceAPI.FindModelVersion( + context.Background(), + ).ParentResourceID(modelID). + Name(versionName).Execute() + if err != nil { + if resp == nil { + return nil, fmt.Errorf("error looking up model version: %w", err) + } + if resp.StatusCode != 200 { + if isOpenAPIErrorOfKind(err, ErrVersionNotFoundForName) { + return nil, fmt.Errorf("%w. model ID: %s version name: %s", ErrVersionNotFound, modelID, versionName) + } + } + // This is a weird case where we got a response and an error that we're unable to handle. + return nil, fmt.Errorf( + "error while querying for a model version: server responded with %s %w", resp.Status, err, + ) + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("failed to find a model version: %s", resp.Status) + } + return version, nil +} + type stringList struct { Items []string `json:"items"` } diff --git a/cli/pkg/modelregistry/errors.go b/cli/pkg/modelregistry/errors.go index a657310f..8509b61f 100644 --- a/cli/pkg/modelregistry/errors.go +++ b/cli/pkg/modelregistry/errors.go @@ -29,10 +29,15 @@ var ( // ErrModelNotFound is returned when a model is not found ErrModelNotFound = errors.New("no registered model found") + // ErrModelNotFoundForName is returned when a model is not found for a given name using FindRegisteredModel + ErrModelNotFoundForName = errors.New("no registered models found") + // ErrVersionExists is returned when a version already exists for a given model ErrVersionExists = errors.New("version already exists") // ErrVersionNotFound is returned when a version is not found ErrVersionNotFound = errors.New("no model version found") + // ErrVersionNotFoundForName is returned when a version is not found for a given name & model ID using FindModelVersion + ErrVersionNotFoundForName = errors.New("no model versions found") // ErrArtifactExists is returned when a model version artifact already exists for a given model version ErrArtifactExists = errors.New("artifact already exists") diff --git a/cli/pkg/pipelines/params.go b/cli/pkg/pipelines/params.go new file mode 100644 index 00000000..9f8bccba --- /dev/null +++ b/cli/pkg/pipelines/params.go @@ -0,0 +1,74 @@ +/* +Copyright 2024. Open Data Hub 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 pipelines + +import ( + "log" + "os" + + "gopkg.in/yaml.v2" +) + +// Param represents a parameter for a pipeline run +type Param struct { + Name string `yaml:"name"` + Value interface{} `yaml:"value"` +} + +// RunParams represents the parameters list for a pipeline run as defined in a YAML file +type RunParams struct { + Params []Param `yaml:"params"` +} + +// GetParamValue returns the value of a parameter by name +func (p *RunParams) GetParamValue(name string) interface{} { + for _, param := range p.Params { + if param.Name == name { + return param.Value + } + } + return nil +} + +// ToSimpleMap converts the RunParams struct to a simple map of string to interface{} +func (p *RunParams) ToSimpleMap() map[string]interface{} { + params := make(map[string]interface{}) + for _, param := range p.Params { + params[param.Name] = param.Value + } + return params +} + +// ReadParams reads a YAML file with pipeline run parameters and returns a RunParams struct +func ReadParams(paramsFile string) (*RunParams, error) { + // Read YAML file + data, err := os.ReadFile(paramsFile) + if err != nil { + log.Fatal("error reading file: ", err) + return nil, err + } + + // Unmarshal YAML to struct + var runParams RunParams + err = yaml.Unmarshal([]byte(data), &runParams) + if err != nil { + log.Fatal("error unmarshalling yaml: ", err) + return nil, err + } + + return &runParams, nil +} diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 00000000..df863d23 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,135 @@ +# ODH Edge CLI + +The ODH Edge CLI is a command line interface that allows you to interact with the ODH Edge platform. It is a tool that allows you to manage your ODH Edge models and images. + +## Overview + +The ODH Edge CLI uses the Model Registry as the source of truth for all models and images. The Model Registry is a central repository that stores all the metadata about the models and images that are available on the ODH Edge platform. The ODH Edge CLI allows you to interact with the Model Registry to perform various operations such as listing and adding models. + +Since building a model image is done via OpenShift Pipelines, all the pipeline parameters that are required to build the model image can be stored in the Model Registry. The ODH Edge CLI allows you to interact with the Model Registry to store and retrieve these pipeline parameters. + +> [!NOTE] +> The Model Registry stores the metadata about the models and images, but it does not store the actual model artifacts. +> The model artifacts are stored in a separate storage location that is accessible to the ODH Edge platform such as an +> S3 bucket or a Git repository. + + +## Building ODH Edge CLI + +To build the ODH Edge CLI, run the following commands from the root of the repository: + +```bash +cd cli +make cli-build +``` + +This will create a binary called `odh` in your current directory. You can move this binary to a location in your PATH to make it easier to run the ODH Edge CLI from anywhere. + +## Environment Setup + +### Prerequisites + +- Kubectl access to the OpenShift core cluster. +- Go (version 1.20 or 1.21, version 1.22 is not supported). +- [Model registry operator](https://github.com/opendatahub-io/model-registry-operator/releases/tag/v0.1.2) version 0.1.2 locally. + +### Installing Model Registry + +To install the Model Registry, follow the following steps: + +1. Change to the `model-registry-operator-0.1.2` directory: + + ```bash + cd model-registry-operator-0.1.2 + ``` +2. Install the CRDs into the cluster: + + ```bash + make install + ``` +3. Deploy the Model Registry operator: + + ```bash + make deploy + ``` +4. Create a Model Registry namespace: + + ```bash + kubectl create ns odh-model-registry + ``` +5. Edit the `config/samples/mysql/modelregistry_v1alpha1_modelregistry.yaml` and change the `spec.rest.serviceRoute` to `enabled`. + +6. Create a Model Registry instance: + + ```bash + kubectl -n odh-model-registry apply -k config/samples/mysql + ``` + +7. Wait for the Model Registry pods to be ready: + + ```bash + kubectl -n odh-model-registry get pods -w + ``` + +8. Create a model-registry route: + + ```bash + oc -n odh-model-registry expose service modelregistry-sample --port http-api + ``` +9. Get the route: + + ```bash + oc -n odh-model-registry get route + ``` + +## Usage + +The following flags are available globally for all commands: + +- `-m, --model-registry-url`: The URL of the Model Registry. This is the URL of the Model Registry service that is + running in the OpenShift cluster. +- `k, --kubeconfig` : The path to the kubeconfig file. This is the kubeconfig file that is used to access the OpenShift + cluster. + +### Listing Models + +To list all the models that are available in the Model Registry, run the following command: + +```bash +odh -m models +``` + +### Adding a Model + +Adding a model to the model registry means adding the metadata about the model to the Model Registry. This metadata includes the model name, a model version and the pipeline parameters that are required to build the model image. + +#### Preparing The Pipeline Parameters + +The parameters can be provided via a yaml file. The yaml file should have the following structure: + +```yaml +params: + - name: + value: + - name: + value: +``` + +This repository provides an example pipeline parameters file in the `cli/examples/params.yaml` file. You can use this file as a template to create your own pipeline parameters file. + +After you have created the pipeline parameters file, you can add the model to the Model Registry by running the following command: + +```bash +odh -m models add -p +``` + +This should print out the message `Model added successfully` if the model was added successfully. + +To verify that the model was added successfully, you can list all the models in the Model Registry by running the following command: + +```bash +odh -m models +``` + + +