diff --git a/.gitattributes b/.gitattributes index 8d3930930b..140de3f68c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -216,6 +216,7 @@ clients/python/src/mr_openapi/models/serving_environment_list.py linguist-genera clients/python/src/mr_openapi/models/serving_environment_update.py linguist-generated=true clients/python/src/mr_openapi/models/sort_order.py linguist-generated=true clients/python/src/mr_openapi/rest.py linguist-generated=true +clients/ui/bff/openapi/docs.go linguist-generated=true internal/converter/generated/embedmd_openapi_converter.gen.go linguist-generated=true internal/converter/generated/openapi_converter.gen.go linguist-generated=true internal/converter/generated/openapi_embedmd_converter.gen.go linguist-generated=true diff --git a/.github/workflows/ui-bff-build.yml b/.github/workflows/ui-bff-build.yml index 4b612f1bf6..51c945b72e 100644 --- a/.github/workflows/ui-bff-build.yml +++ b/.github/workflows/ui-bff-build.yml @@ -47,6 +47,10 @@ jobs: working-directory: clients/ui/bff run: make build + - name: Generate OpenAPI spec + working-directory: clients/ui/bff + run: make openapi + - name: Check if there are uncommitted file changes working-directory: clients/ui/bff run: | diff --git a/clients/ui/Dockerfile b/clients/ui/Dockerfile index 7c1936b281..e1f1fba109 100644 --- a/clients/ui/Dockerfile +++ b/clients/ui/Dockerfile @@ -43,6 +43,7 @@ RUN go mod download # Copy the go source files COPY ${BFF_SOURCE_CODE}/cmd/ cmd/ COPY ${BFF_SOURCE_CODE}/internal/ internal/ +COPY ${BFF_SOURCE_CODE}/openapi/ openapi/ # Build the Go application RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o bff ./cmd diff --git a/clients/ui/Dockerfile.standalone b/clients/ui/Dockerfile.standalone index 131fcafefd..d2a5924daf 100644 --- a/clients/ui/Dockerfile.standalone +++ b/clients/ui/Dockerfile.standalone @@ -43,6 +43,7 @@ RUN go mod download # Copy the go source files COPY ${BFF_SOURCE_CODE}/cmd/ cmd/ COPY ${BFF_SOURCE_CODE}/internal/ internal/ +COPY ${BFF_SOURCE_CODE}/openapi/ openapi/ # Build the Go application RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o bff ./cmd diff --git a/clients/ui/bff/Makefile b/clients/ui/bff/Makefile index 8c26e1c275..88c221decf 100644 --- a/clients/ui/bff/Makefile +++ b/clients/ui/bff/Makefile @@ -52,7 +52,7 @@ test: fmt vet envtest ## Runs the full test suite. go test ./... .PHONY: build -build: fmt vet test ## Builds the project to produce a binary executable. +build: fmt vet openapi test ## Builds the project to produce a binary executable. OpenAPI spec is generated before build. ifeq ($(DEBUG), true) ## If DEBUG is true, build with debugging symbols go build $(GCFLAGS_DEBUG) -o bin/bff ./cmd else @@ -60,7 +60,7 @@ else endif .PHONY: run -run: fmt vet envtest ## Runs the project. +run: fmt vet openapi envtest ## Runs the project. OpenAPI spec is generated before run. trap 'exit 0' INT; \ ENVTEST_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ go run ./cmd --port=$(PORT) --auth-method=${AUTH_METHOD} --auth-token-header=$(AUTH_TOKEN_HEADER) --auth-token-prefix="$(AUTH_TOKEN_PREFIX)" --static-assets-dir=$(STATIC_ASSETS_DIR) --mock-k8s-client=$(MOCK_K8S_CLIENT) --mock-mr-client=$(MOCK_MR_CLIENT) --mock-mr-catalog-client=$(MOCK_MR_CATALOG_CLIENT) --dev-mode=$(DEV_MODE) --dev-mode-model-registry-port=$(DEV_MODE_MODEL_REGISTRY_PORT) --dev-mode-catalog-port=$(DEV_MODE_CATALOG_PORT) --deployment-mode=$(DEPLOYMENT_MODE) --log-level=$(LOG_LEVEL) --allowed-origins=$(ALLOWED_ORIGINS) --insecure-skip-verify=$(INSECURE_SKIP_VERIFY) @@ -90,6 +90,23 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) +SWAG_VERSION ?= v1.16.4 +SWAG ?= $(LOCALBIN)/swag-$(SWAG_VERSION) + +.PHONY: swag +swag: $(SWAG) ## Download swag locally if necessary. +$(SWAG): $(LOCALBIN) + $(call go-install-tool,$(SWAG),github.com/swaggo/swag/cmd/swag,$(SWAG_VERSION)) + +ALL_GO_DIRS := $(shell find . -type f -name '*.go' -exec dirname {} \; | sed 's|^\./||' | sort -u) +ALL_GO_DIRS_NO_CMD := $(shell echo "$(ALL_GO_DIRS)" | tr ' ' '\n' | grep -v '^cmd$$' | paste -sd, -) +SWAG_DIRS := cmd,$(ALL_GO_DIRS_NO_CMD) + +.PHONY: openapi +openapi: swag ## Generate OpenAPI spec from annotations + $(SWAG) fmt -g cmd/main.go -d $(SWAG_DIRS) + $(SWAG) init --parseDependency -q -g main.go -d $(SWAG_DIRS) -o openapi --outputTypes go,json,yaml --requiredByDefault + # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary (ideally with version) diff --git a/clients/ui/bff/README.md b/clients/ui/bff/README.md index 3b10306738..e782d3f9b8 100644 --- a/clients/ui/bff/README.md +++ b/clients/ui/bff/README.md @@ -82,6 +82,8 @@ make docker-build See the [OpenAPI specification](../api/openapi/mod-arch.yaml) for a complete list of endpoints. +> **Note:** We are in the process of moving from a manually maintained OpenAPI spec to a generated one. For a short period we have both: the hand-maintained [mod-arch.yaml](../api/openapi/mod-arch.yaml) and the generated [swagger.yaml](openapi/swagger.yaml) / [swagger.json](openapi/swagger.json) (produced from Go code via swaggo). The goal is to generate the OpenAPI spec from the codebase rather than managing it manually; once the transition is complete, the generated spec will be the single source of truth. + ### Sample local calls You will need to inject your requests with a `kubeflow-userid` header and namespace for authorization purposes. diff --git a/clients/ui/bff/cmd/main.go b/clients/ui/bff/cmd/main.go index 6b8165fef8..36a60329d3 100644 --- a/clients/ui/bff/cmd/main.go +++ b/clients/ui/bff/cmd/main.go @@ -1,3 +1,13 @@ +// @title Model Registry BFF REST API +// @version 1.0.0 +// @description REST API for Model Registry BFF +// @license.name Apache 2.0 +// @license.url https://www.apache.org/licenses/LICENSE-2.0 + +// @host localhost:4000 +// @BasePath /api/v1 +// @schemes http https + package main import ( @@ -15,6 +25,8 @@ import ( "net/http" "os" "time" + + _ "github.com/kubeflow/model-registry/ui/bff/openapi" // swagger docs for Swagger UI ) func main() { diff --git a/clients/ui/bff/go.mod b/clients/ui/bff/go.mod index c81503950b..9881c59753 100644 --- a/clients/ui/bff/go.mod +++ b/clients/ui/bff/go.mod @@ -11,6 +11,8 @@ require ( github.com/onsi/gomega v1.38.2 github.com/rs/cors v1.11.1 github.com/stretchr/testify v1.11.1 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/swaggo/swag v1.16.4 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.35.3 k8s.io/apimachinery v0.35.3 @@ -19,6 +21,7 @@ require ( ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -30,6 +33,7 @@ require ( github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect @@ -48,6 +52,7 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect diff --git a/clients/ui/bff/go.sum b/clients/ui/bff/go.sum index 06fb6540d2..271cd6f2f8 100644 --- a/clients/ui/bff/go.sum +++ b/clients/ui/bff/go.sum @@ -1,3 +1,5 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -6,6 +8,7 @@ github.com/brianvoe/gofakeit/v7 v7.7.3 h1:RWOATEGpJ5EVg2nN8nlaEyaV/aB4d6c3GqYrbq github.com/brianvoe/gofakeit/v7 v7.7.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -28,10 +31,17 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -55,12 +65,18 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubeflow/model-registry/pkg/openapi v0.3.2 h1:t/H+zxHiwcPGUITG/fWHUrTrJwoi9IlVa7vmzZI1eZk= github.com/kubeflow/model-registry/pkg/openapi v0.3.2/go.mod h1:0V0wF5hGlLDSNS+on0MTnEOFiubfVYNc7QhuthKBu+8= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= @@ -75,6 +91,7 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -100,8 +117,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= +github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -145,12 +169,19 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index 9039ded015..7ecde162ef 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -89,6 +89,10 @@ const ( McpServerFilterOptionListPath = McpServerCatalogPathPrefix + "/mcp_servers_filter_options" McpServerPath = McpServerListPath + "/:" + McpServerId McpServersToolListPath = McpServerPath + "/tools" + + // Swagger UI (interactive API docs) + SwaggerPath = ApiPathPrefix + "/swagger" + SwaggerDocPath = SwaggerPath + "/doc.json" ) type App struct { @@ -277,6 +281,11 @@ func (app *App) Routes() http.Handler { apiRouter.POST(CheckNamespaceRegistryAccessPath, app.CheckNamespaceRegistryAccessHandler) apiRouter.GET(ModelRegistryListPath, app.AttachNamespace(app.RequireListServiceAccessInNamespace(app.GetAllModelRegistriesHandler))) + // Swagger UI (interactive API docs) — only in dev mode + if app.config.DevMode { + apiRouter.GET(SwaggerPath+"/*filepath", app.GetSwaggerHandler) + } + // Enable these routes in all cases except Kubeflow integration mode // (Kubeflow integration mode is when DeploymentMode is kubeflow) isKubeflowIntegrationMode := app.config.DeploymentMode.IsKubeflowMode() diff --git a/clients/ui/bff/internal/api/healthcheck__handler_test.go b/clients/ui/bff/internal/api/healthcheck__handler_test.go index de2ecc30c0..67f4904d2c 100644 --- a/clients/ui/bff/internal/api/healthcheck__handler_test.go +++ b/clients/ui/bff/internal/api/healthcheck__handler_test.go @@ -9,7 +9,7 @@ import ( "github.com/kubeflow/model-registry/ui/bff/internal/config" "github.com/kubeflow/model-registry/ui/bff/internal/mocks" - "github.com/kubeflow/model-registry/ui/bff/internal/models" + "github.com/kubeflow/model-registry/ui/bff/internal/models/healthcheck" "github.com/kubeflow/model-registry/ui/bff/internal/repositories" "github.com/stretchr/testify/assert" ) @@ -35,15 +35,15 @@ func TestHealthCheckHandler(t *testing.T) { body, err := io.ReadAll(rs.Body) assert.NoError(t, err) - var healthCheckRes models.HealthCheckModel + var healthCheckRes healthcheck.HealthCheckModel err = json.Unmarshal(body, &healthCheckRes) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) - expected := models.HealthCheckModel{ + expected := healthcheck.HealthCheckModel{ Status: "available", - SystemInfo: models.SystemInfo{ + SystemInfo: healthcheck.SystemInfo{ Version: Version, }, } diff --git a/clients/ui/bff/internal/api/healthcheck_handler.go b/clients/ui/bff/internal/api/healthcheck_handler.go index a62e10af3b..53e6ef16b4 100644 --- a/clients/ui/bff/internal/api/healthcheck_handler.go +++ b/clients/ui/bff/internal/api/healthcheck_handler.go @@ -4,9 +4,26 @@ import ( "net/http" "github.com/julienschmidt/httprouter" + + // imported for swag documentation + _ "github.com/kubeflow/model-registry/ui/bff/internal/models/healthcheck" ) -func (app *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +// HealthcheckHandler returns the health status of the application. +// +// @Summary Returns the health status of the application +// @Description Provides a healthcheck response indicating the status of key services. +// @Tags healthcheck +// @ID getHealthcheck +// @Produce application/json +// @Success 200 {object} healthcheck.HealthCheckModel "Successful healthcheck response" +// @Failure 401 {object} ErrorEnvelope "Unauthorized. Authentication is required." +// @Failure 403 {object} ErrorEnvelope "Forbidden. User does not have permission to access the resource." +// @Failure 404 {object} ErrorEnvelope "Not Found. Resource does not exist." +// @Failure 422 {object} ErrorEnvelope "Unprocessable Entity. Validation error." +// @Failure 500 {object} ErrorEnvelope "Internal server error" +// @Router /healthcheck [get] +func (app *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { healthCheck, err := app.repositories.HealthCheck.HealthCheck(Version) if err != nil { app.serverErrorResponse(w, r, err) diff --git a/clients/ui/bff/internal/api/swagger_handler.go b/clients/ui/bff/internal/api/swagger_handler.go new file mode 100644 index 0000000000..4c461e415e --- /dev/null +++ b/clients/ui/bff/internal/api/swagger_handler.go @@ -0,0 +1,34 @@ +/* +Copyright 2026. + +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 api + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + httpSwagger "github.com/swaggo/http-swagger/v2" +) + +// GetSwaggerHandler serves the Swagger UI for interactive API documentation. +func (app *App) GetSwaggerHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpSwagger.Handler( + httpSwagger.URL(SwaggerDocPath), + httpSwagger.DeepLinking(true), + httpSwagger.DocExpansion("list"), + httpSwagger.DomID("swagger-ui"), + ).ServeHTTP(w, r) +} diff --git a/clients/ui/bff/internal/models/health_check.go b/clients/ui/bff/internal/models/healthcheck/healthcheck.go similarity index 89% rename from clients/ui/bff/internal/models/health_check.go rename to clients/ui/bff/internal/models/healthcheck/healthcheck.go index daf9e72d25..f4f73229f1 100644 --- a/clients/ui/bff/internal/models/health_check.go +++ b/clients/ui/bff/internal/models/healthcheck/healthcheck.go @@ -1,4 +1,4 @@ -package models +package healthcheck type SystemInfo struct { Version string `json:"version"` diff --git a/clients/ui/bff/internal/repositories/health_check.go b/clients/ui/bff/internal/repositories/health_check.go index a012b5116c..0bc46b8c12 100644 --- a/clients/ui/bff/internal/repositories/health_check.go +++ b/clients/ui/bff/internal/repositories/health_check.go @@ -1,6 +1,6 @@ package repositories -import "github.com/kubeflow/model-registry/ui/bff/internal/models" +import "github.com/kubeflow/model-registry/ui/bff/internal/models/healthcheck" type HealthCheckRepository struct{} @@ -8,13 +8,11 @@ func NewHealthCheckRepository() *HealthCheckRepository { return &HealthCheckRepository{} } -func (r *HealthCheckRepository) HealthCheck(version string) (models.HealthCheckModel, error) { - var res = models.HealthCheckModel{ +func (r *HealthCheckRepository) HealthCheck(version string) (healthcheck.HealthCheckModel, error) { + return healthcheck.HealthCheckModel{ Status: "available", - SystemInfo: models.SystemInfo{ + SystemInfo: healthcheck.SystemInfo{ Version: version, }, - } - - return res, nil + }, nil } diff --git a/clients/ui/bff/openapi/docs.go b/clients/ui/bff/openapi/docs.go new file mode 100644 index 0000000000..17590eba6d --- /dev/null +++ b/clients/ui/bff/openapi/docs.go @@ -0,0 +1,146 @@ +// Package openapi Code generated by swaggo/swag. DO NOT EDIT +package openapi + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/healthcheck": { + "get": { + "description": "Provides a healthcheck response indicating the status of key services.", + "produces": [ + "application/json" + ], + "tags": [ + "healthcheck" + ], + "summary": "Returns the health status of the application", + "operationId": "getHealthcheck", + "responses": { + "200": { + "description": "Successful healthcheck response", + "schema": { + "$ref": "#/definitions/healthcheck.HealthCheckModel" + } + }, + "401": { + "description": "Unauthorized. Authentication is required.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden. User does not have permission to access the resource.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found. Resource does not exist.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "422": { + "description": "Unprocessable Entity. Validation error.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + } + } + } + } + }, + "definitions": { + "api.ErrorEnvelope": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "$ref": "#/definitions/httpclient.HTTPError" + } + } + }, + "healthcheck.HealthCheckModel": { + "type": "object", + "required": [ + "status", + "system_info" + ], + "properties": { + "status": { + "type": "string" + }, + "system_info": { + "$ref": "#/definitions/healthcheck.SystemInfo" + } + } + }, + "healthcheck.SystemInfo": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + } + }, + "httpclient.HTTPError": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0.0", + Host: "localhost:4000", + BasePath: "/api/v1", + Schemes: []string{"http", "https"}, + Title: "Model Registry BFF REST API", + Description: "REST API for Model Registry BFF", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/clients/ui/bff/openapi/swagger.json b/clients/ui/bff/openapi/swagger.json new file mode 100644 index 0000000000..871d72e636 --- /dev/null +++ b/clients/ui/bff/openapi/swagger.json @@ -0,0 +1,126 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "REST API for Model Registry BFF", + "title": "Model Registry BFF REST API", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "version": "1.0.0" + }, + "host": "localhost:4000", + "basePath": "/api/v1", + "paths": { + "/healthcheck": { + "get": { + "description": "Provides a healthcheck response indicating the status of key services.", + "produces": [ + "application/json" + ], + "tags": [ + "healthcheck" + ], + "summary": "Returns the health status of the application", + "operationId": "getHealthcheck", + "responses": { + "200": { + "description": "Successful healthcheck response", + "schema": { + "$ref": "#/definitions/healthcheck.HealthCheckModel" + } + }, + "401": { + "description": "Unauthorized. Authentication is required.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden. User does not have permission to access the resource.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found. Resource does not exist.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "422": { + "description": "Unprocessable Entity. Validation error.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + } + } + } + } + }, + "definitions": { + "api.ErrorEnvelope": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "$ref": "#/definitions/httpclient.HTTPError" + } + } + }, + "healthcheck.HealthCheckModel": { + "type": "object", + "required": [ + "status", + "system_info" + ], + "properties": { + "status": { + "type": "string" + }, + "system_info": { + "$ref": "#/definitions/healthcheck.SystemInfo" + } + } + }, + "healthcheck.SystemInfo": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + } + }, + "httpclient.HTTPError": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/clients/ui/bff/openapi/swagger.yaml b/clients/ui/bff/openapi/swagger.yaml new file mode 100644 index 0000000000..bef74810e2 --- /dev/null +++ b/clients/ui/bff/openapi/swagger.yaml @@ -0,0 +1,84 @@ +basePath: /api/v1 +definitions: + api.ErrorEnvelope: + properties: + error: + $ref: '#/definitions/httpclient.HTTPError' + required: + - error + type: object + healthcheck.HealthCheckModel: + properties: + status: + type: string + system_info: + $ref: '#/definitions/healthcheck.SystemInfo' + required: + - status + - system_info + type: object + healthcheck.SystemInfo: + properties: + version: + type: string + required: + - version + type: object + httpclient.HTTPError: + properties: + code: + type: string + message: + type: string + required: + - code + - message + type: object +host: localhost:4000 +info: + contact: {} + description: REST API for Model Registry BFF + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + title: Model Registry BFF REST API + version: 1.0.0 +paths: + /healthcheck: + get: + description: Provides a healthcheck response indicating the status of key services. + operationId: getHealthcheck + produces: + - application/json + responses: + "200": + description: Successful healthcheck response + schema: + $ref: '#/definitions/healthcheck.HealthCheckModel' + "401": + description: Unauthorized. Authentication is required. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "403": + description: Forbidden. User does not have permission to access the resource. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "404": + description: Not Found. Resource does not exist. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "422": + description: Unprocessable Entity. Validation error. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ErrorEnvelope' + summary: Returns the health status of the application + tags: + - healthcheck +schemes: +- http +- https +swagger: "2.0"