diff --git a/cmd/hub/handlers/handlers.go b/cmd/hub/handlers/handlers.go index ac6d64aeb..0d3e882d8 100644 --- a/cmd/hub/handlers/handlers.go +++ b/cmd/hub/handlers/handlers.go @@ -264,6 +264,31 @@ func (h *Handlers) setupRouter() { r.With(h.Users.RequireLogin).Post("/images", h.Static.SaveImage) }) + // Monocular compatible search API + // + // This endpoint provides a Monocular compatible search API that the Helm + // CLI search subcommand can use. The goal is to facilitate the transition + // from the Helm Hub to Artifact Hub, allowing the existing Helm tooling to + // continue working without modifications. This is a temporary solution and + // future Helm CLI versions should use the generic Artifact Hub search API. + r.Get("/api/chartsvc/v1/charts/search", h.Packages.SearchMonocular) + + // Monocular charts url redirect endpoint + // + // This endpoint is a helper related to the Monocular search one above. At + // the moment Helm CLI builds charts urls coming from the Helm Hub using + // this layout. This cannot be changed for previous versions out there, so + // this endpoint handles the redirection to the package URL in Artifact Hub. + // The monocular compatible search API endpoint that we provide now returns + // the package url to facilitate that future versions of Helm can use it. + r.Get("/charts/{repoName}/{packageName}", func(w http.ResponseWriter, r *http.Request) { + pkgPath := fmt.Sprintf("/packages/helm/%s/%s", + chi.URLParam(r, "repoName"), + chi.URLParam(r, "packageName"), + ) + http.Redirect(w, r, pkgPath, http.StatusMovedPermanently) + }) + // Oauth providers := make([]string, 0, len(h.cfg.GetStringMap("server.oauth"))) for provider := range h.cfg.GetStringMap("server.oauth") { diff --git a/cmd/hub/handlers/pkg/handlers.go b/cmd/hub/handlers/pkg/handlers.go index 209d8a10e..486dad3df 100644 --- a/cmd/hub/handlers/pkg/handlers.go +++ b/cmd/hub/handlers/pkg/handlers.go @@ -195,8 +195,7 @@ func (h *Handlers) RssFeed(w http.ResponseWriter, r *http.Request) { _ = feed.WriteRss(w) } -// Search is an http handler used to searchPackages for packages in the hub -// database. +// Search is an http handler used to search for packages in the hub database. func (h *Handlers) Search(w http.ResponseWriter, r *http.Request) { input, err := buildSearchInput(r.URL.Query()) if err != nil { @@ -214,6 +213,20 @@ func (h *Handlers) Search(w http.ResponseWriter, r *http.Request) { helpers.RenderJSON(w, dataJSON, helpers.DefaultAPICacheMaxAge, http.StatusOK) } +// SearchMonocular is an http handler used to search for packages in the hub +// database that is compatible with the Monocular search API. +func (h *Handlers) SearchMonocular(w http.ResponseWriter, r *http.Request) { + baseURL := h.cfg.GetString("server.baseURL") + tsQueryWeb := r.FormValue("q") + dataJSON, err := h.pkgManager.SearchMonocularJSON(r.Context(), baseURL, tsQueryWeb) + if err != nil { + h.logger.Error().Err(err).Str("query", r.URL.RawQuery).Str("method", "SearchMonocular").Send() + helpers.RenderErrorJSON(w, err) + return + } + helpers.RenderJSON(w, dataJSON, helpers.DefaultAPICacheMaxAge, http.StatusOK) +} + // ToggleStar is an http handler used to toggle the star on a given package. func (h *Handlers) ToggleStar(w http.ResponseWriter, r *http.Request) { packageID := chi.URLParam(r, "packageID") diff --git a/cmd/hub/handlers/pkg/handlers_test.go b/cmd/hub/handlers/pkg/handlers_test.go index 77cef811b..403657b74 100644 --- a/cmd/hub/handlers/pkg/handlers_test.go +++ b/cmd/hub/handlers/pkg/handlers_test.go @@ -556,6 +556,41 @@ func TestSearch(t *testing.T) { }) } +func TestSearchMonocular(t *testing.T) { + t.Run("search succeeded", func(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/?q=text", nil) + + hw := newHandlersWrapper() + hw.pm.On("SearchMonocularJSON", r.Context(), "baseURL", "text").Return([]byte("dataJSON"), nil) + hw.h.SearchMonocular(w, r) + resp := w.Result() + defer resp.Body.Close() + h := resp.Header + data, _ := ioutil.ReadAll(resp.Body) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", h.Get("Content-Type")) + assert.Equal(t, helpers.BuildCacheControlHeader(helpers.DefaultAPICacheMaxAge), h.Get("Cache-Control")) + assert.Equal(t, []byte("dataJSON"), data) + hw.pm.AssertExpectations(t) + }) + + t.Run("search failed", func(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/?q=text", nil) + + hw := newHandlersWrapper() + hw.pm.On("SearchMonocularJSON", r.Context(), "baseURL", "text").Return(nil, tests.ErrFakeDatabaseFailure) + hw.h.SearchMonocular(w, r) + resp := w.Result() + defer resp.Body.Close() + + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + hw.pm.AssertExpectations(t) + }) +} + func TestToggleStar(t *testing.T) { rctx := &chi.Context{ URLParams: chi.RouteParams{ diff --git a/database/migrations/functions/001_load_functions.sql b/database/migrations/functions/001_load_functions.sql index a74b0f0b4..bb6ed049d 100644 --- a/database/migrations/functions/001_load_functions.sql +++ b/database/migrations/functions/001_load_functions.sql @@ -32,6 +32,7 @@ {{ template "packages/get_random_packages.sql" }} {{ template "packages/register_package.sql" }} {{ template "packages/search_packages.sql" }} +{{ template "packages/search_packages_monocular.sql" }} {{ template "packages/semver_gt.sql" }} {{ template "packages/semver_gte.sql" }} {{ template "packages/toggle_star.sql" }} diff --git a/database/migrations/functions/packages/search_packages_monocular.sql b/database/migrations/functions/packages/search_packages_monocular.sql new file mode 100644 index 000000000..ef00f7407 --- /dev/null +++ b/database/migrations/functions/packages/search_packages_monocular.sql @@ -0,0 +1,61 @@ +-- search_packages_monocular searchs packages in the database that match the +-- criteria in the query provided, returning results in a format that is +-- compatible with the Monocular search API. +create or replace function search_packages_monocular(p_base_url text, p_tsquery_web text) +returns setof json as $$ +declare + v_tsquery_web tsquery := websearch_to_tsquery(p_tsquery_web); +begin + return query + with packages_found as ( + select + p.normalized_name as package_name, + s.description, + s.version, + s.app_version, + r.name as repository_name, + (case when p_tsquery_web <> '' then + ts_rank(ts_filter(tsdoc, '{a}'), v_tsquery_web, 1) + + ts_rank('{0.1, 0.2, 0.2, 1.0}', ts_filter(tsdoc, '{b,c}'), v_tsquery_web) + else 1 end) as rank + from package p + join snapshot s using (package_id) + join repository r using (repository_id) + where r.repository_kind_id = 0 -- Helm + and s.version = p.latest_version + and (s.deprecated is null or s.deprecated = false) + and + case when p_tsquery_web <> '' then + v_tsquery_web @@ p.tsdoc + else true end + order by rank desc, package_name asc + ) + select json_build_object( + 'data', ( + select coalesce(json_agg(json_build_object( + 'id', format('%s/%s', repository_name, package_name), + 'artifactHub', json_build_object( + 'packageUrl', format( + '%s/packages/helm/%s/%s', + p_base_url, + repository_name, + package_name + ) + ), + 'attributes', json_build_object( + 'description', description + ), + 'relationships', json_build_object( + 'latestChartVersion', json_build_object( + 'data', json_build_object( + 'version', version, + 'app_version', app_version + ) + ) + ) + )), '[]') + from packages_found + ) + ); +end +$$ language plpgsql; diff --git a/database/tests/functions/packages/search_packages_monocular.sql b/database/tests/functions/packages/search_packages_monocular.sql new file mode 100644 index 000000000..dd4b7e2e7 --- /dev/null +++ b/database/tests/functions/packages/search_packages_monocular.sql @@ -0,0 +1,78 @@ +-- Start transaction and plan tests +begin; +select plan(3); + +-- Declare some variables +\set user1ID '00000000-0000-0000-0000-000000000001' +\set repo1ID '00000000-0000-0000-0000-000000000001' +\set package1ID '00000000-0000-0000-0000-000000000001' + +-- No packages at this point +select is( + search_packages_monocular('https://artifacthub.io', 'package1')::jsonb, + '{"data": []}'::jsonb, + 'TsQueryWeb: package1 | No packages expected' +); + +-- Seed some data +insert into "user" (user_id, alias, email) values (:'user1ID', 'user1', 'user1@email.com'); +insert into repository (repository_id, name, display_name, url, repository_kind_id, user_id) +values (:'repo1ID', 'repo1', 'Repo 1', 'https://repo1.com', 0, :'user1ID'); +insert into package ( + package_id, + name, + latest_version, + tsdoc, + repository_id +) values ( + :'package1ID', + 'package1', + '1.0.0', + generate_package_tsdoc('package1', null, 'description', '{"kw1", "kw2"}', '{"repo1"}', '{"user1"}'), + :'repo1ID' +); +insert into snapshot ( + package_id, + version, + description, + app_version +) values ( + :'package1ID', + '1.0.0', + 'description', + '12.1.0' +); + +-- Run some tests +select is( + search_packages_monocular('https://artifacthub.io', 'package1')::jsonb, + '{ + "data": [{ + "id": "repo1/package1", + "artifactHub": { + "packageUrl": "https://artifacthub.io/packages/helm/repo1/package1" + }, + "attributes": { + "description": "description" + }, + "relationships": { + "latestChartVersion": { + "data": { + "version": "1.0.0", + "app_version": "12.1.0" + } + } + } + }] + }'::jsonb, + 'TsQueryWeb: package1 | Package1 expected' +); +select is( + search_packages_monocular('https://artifacthub.io', 'package2')::jsonb, + '{"data": []}'::jsonb, + 'TsQueryWeb: package2 | No packages expected' +); + +-- Finish tests and rollback transaction +select * from finish(); +rollback; diff --git a/database/tests/schema/schema.sql b/database/tests/schema/schema.sql index 955afa54c..7e39dab51 100644 --- a/database/tests/schema/schema.sql +++ b/database/tests/schema/schema.sql @@ -1,6 +1,6 @@ -- Start transaction and plan tests begin; -select plan(115); +select plan(116); -- Check default_text_search_config is correct select results_eq( @@ -338,6 +338,7 @@ select has_function('get_packages_stats'); select has_function('get_random_packages'); select has_function('register_package'); select has_function('search_packages'); +select has_function('search_packages_monocular'); select has_function('semver_gt'); select has_function('semver_gte'); select has_function('toggle_star'); diff --git a/docs/api/custom-styles.css b/docs/api/custom-styles.css index f4519e9bf..a16275f40 100644 --- a/docs/api/custom-styles.css +++ b/docs/api/custom-styles.css @@ -9,6 +9,10 @@ height: 50px; } +.topbar-wrapper img { + opacity: 0; +} + @media only screen and (max-width: 575.98px) { .opblock-section-request-body .opblock-section-header { flex-direction: column; diff --git a/docs/api/index.html b/docs/api/index.html index f43144e8d..7be01db80 100644 --- a/docs/api/index.html +++ b/docs/api/index.html @@ -44,17 +44,20 @@