Skip to content

Commit

Permalink
Add Monocular compatible search API endpoint (#598)
Browse files Browse the repository at this point in the history
Closes #593

Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
Co-authored-by: Sergio Castaño Arteaga <tegioz@icloud.com>
Co-authored-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
  • Loading branch information
tegioz and cynthia-sg authored Aug 21, 2020
1 parent ba35434 commit e07dd82
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 9 deletions.
25 changes: 25 additions & 0 deletions cmd/hub/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
17 changes: 15 additions & 2 deletions cmd/hub/handlers/pkg/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
Expand Down
35 changes: 35 additions & 0 deletions cmd/hub/handlers/pkg/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
1 change: 1 addition & 0 deletions database/migrations/functions/001_load_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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" }}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
78 changes: 78 additions & 0 deletions database/tests/functions/packages/search_packages_monocular.sql
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion database/tests/schema/schema.sql
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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');
Expand Down
4 changes: 4 additions & 0 deletions docs/api/custom-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 7 additions & 4 deletions docs/api/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,20 @@
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "openapi.yaml",
urls: [
{url: "openapi.yaml", name: "Artifact Hub API"},
{url: "openapi-monocular.yaml", name: "Monocular compatible search API"},
],
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
SwaggerUIStandalonePreset,
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
SwaggerUIBundle.plugins.DownloadUrl,
],
layout: "BaseLayout"
layout: "StandaloneLayout"
})
window.ui = ui
}
Expand Down
Loading

0 comments on commit e07dd82

Please sign in to comment.