Skip to content

Commit 4ecc33f

Browse files
tegiozcynthia-sg
andcommitted
Add Monocular compatible search API endpoint
Closes #593 Signed-off-by: Sergio Castaño Arteaga <[email protected]> Signed-off-by: Cintia Sanchez Garcia <[email protected]> Co-authored-by: Sergio Castaño Arteaga <[email protected]> Co-authored-by: Cintia Sanchez Garcia <[email protected]>
1 parent ba35434 commit 4ecc33f

15 files changed

+401
-9
lines changed

cmd/hub/handlers/handlers.go

+25
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,31 @@ func (h *Handlers) setupRouter() {
264264
r.With(h.Users.RequireLogin).Post("/images", h.Static.SaveImage)
265265
})
266266

267+
// Monocular compatible search API
268+
//
269+
// This endpoint provides a Monocular compatible search API that the Helm
270+
// CLI search subcommand can use. The goal is to facilitate the transition
271+
// from the Helm Hub to Artifact Hub, allowing the existing Helm tooling to
272+
// continue working without modifications. This is a temporary solution and
273+
// future Helm CLI versions should use the generic Artifact Hub search API.
274+
r.Get("/api/chartsvc/v1/charts/search", h.Packages.SearchMonocular)
275+
276+
// Monocular charts url redirect endpoint
277+
//
278+
// This endpoint is a helper related to the Monocular search one above. At
279+
// the moment Helm CLI builds charts urls coming from the Helm Hub using
280+
// this layout. This cannot be changed for previous versions out there, so
281+
// this endpoint handles the redirection to the package URL in Artifact Hub.
282+
// The monocular compatible search API endpoint that we provide now returns
283+
// the package url to facilitate that future versions of Helm can use it.
284+
r.Get("/charts/{repoName}/{packageName}", func(w http.ResponseWriter, r *http.Request) {
285+
pkgPath := fmt.Sprintf("/packages/helm/%s/%s",
286+
chi.URLParam(r, "repoName"),
287+
chi.URLParam(r, "packageName"),
288+
)
289+
http.Redirect(w, r, pkgPath, http.StatusMovedPermanently)
290+
})
291+
267292
// Oauth
268293
providers := make([]string, 0, len(h.cfg.GetStringMap("server.oauth")))
269294
for provider := range h.cfg.GetStringMap("server.oauth") {

cmd/hub/handlers/pkg/handlers.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,7 @@ func (h *Handlers) RssFeed(w http.ResponseWriter, r *http.Request) {
195195
_ = feed.WriteRss(w)
196196
}
197197

198-
// Search is an http handler used to searchPackages for packages in the hub
199-
// database.
198+
// Search is an http handler used to search for packages in the hub database.
200199
func (h *Handlers) Search(w http.ResponseWriter, r *http.Request) {
201200
input, err := buildSearchInput(r.URL.Query())
202201
if err != nil {
@@ -214,6 +213,20 @@ func (h *Handlers) Search(w http.ResponseWriter, r *http.Request) {
214213
helpers.RenderJSON(w, dataJSON, helpers.DefaultAPICacheMaxAge, http.StatusOK)
215214
}
216215

216+
// SearchMonocular is an http handler used to search for packages in the hub
217+
// database that is compatible with the Monocular search API.
218+
func (h *Handlers) SearchMonocular(w http.ResponseWriter, r *http.Request) {
219+
baseURL := h.cfg.GetString("server.baseURL")
220+
tsQueryWeb := r.FormValue("q")
221+
dataJSON, err := h.pkgManager.SearchMonocularJSON(r.Context(), baseURL, tsQueryWeb)
222+
if err != nil {
223+
h.logger.Error().Err(err).Str("query", r.URL.RawQuery).Str("method", "SearchMonocular").Send()
224+
helpers.RenderErrorJSON(w, err)
225+
return
226+
}
227+
helpers.RenderJSON(w, dataJSON, helpers.DefaultAPICacheMaxAge, http.StatusOK)
228+
}
229+
217230
// ToggleStar is an http handler used to toggle the star on a given package.
218231
func (h *Handlers) ToggleStar(w http.ResponseWriter, r *http.Request) {
219232
packageID := chi.URLParam(r, "packageID")

cmd/hub/handlers/pkg/handlers_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,41 @@ func TestSearch(t *testing.T) {
556556
})
557557
}
558558

559+
func TestSearchMonocular(t *testing.T) {
560+
t.Run("search succeeded", func(t *testing.T) {
561+
w := httptest.NewRecorder()
562+
r, _ := http.NewRequest("GET", "/?q=text", nil)
563+
564+
hw := newHandlersWrapper()
565+
hw.pm.On("SearchMonocularJSON", r.Context(), "baseURL", "text").Return([]byte("dataJSON"), nil)
566+
hw.h.SearchMonocular(w, r)
567+
resp := w.Result()
568+
defer resp.Body.Close()
569+
h := resp.Header
570+
data, _ := ioutil.ReadAll(resp.Body)
571+
572+
assert.Equal(t, http.StatusOK, resp.StatusCode)
573+
assert.Equal(t, "application/json", h.Get("Content-Type"))
574+
assert.Equal(t, helpers.BuildCacheControlHeader(helpers.DefaultAPICacheMaxAge), h.Get("Cache-Control"))
575+
assert.Equal(t, []byte("dataJSON"), data)
576+
hw.pm.AssertExpectations(t)
577+
})
578+
579+
t.Run("search failed", func(t *testing.T) {
580+
w := httptest.NewRecorder()
581+
r, _ := http.NewRequest("GET", "/?q=text", nil)
582+
583+
hw := newHandlersWrapper()
584+
hw.pm.On("SearchMonocularJSON", r.Context(), "baseURL", "text").Return(nil, tests.ErrFakeDatabaseFailure)
585+
hw.h.SearchMonocular(w, r)
586+
resp := w.Result()
587+
defer resp.Body.Close()
588+
589+
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
590+
hw.pm.AssertExpectations(t)
591+
})
592+
}
593+
559594
func TestToggleStar(t *testing.T) {
560595
rctx := &chi.Context{
561596
URLParams: chi.RouteParams{

database/migrations/functions/001_load_functions.sql

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
{{ template "packages/get_random_packages.sql" }}
3333
{{ template "packages/register_package.sql" }}
3434
{{ template "packages/search_packages.sql" }}
35+
{{ template "packages/search_packages_monocular.sql" }}
3536
{{ template "packages/semver_gt.sql" }}
3637
{{ template "packages/semver_gte.sql" }}
3738
{{ template "packages/toggle_star.sql" }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-- search_packages_monocular searchs packages in the database that match the
2+
-- criteria in the query provided, returning results in a format that is
3+
-- compatible with the Monocular search API.
4+
create or replace function search_packages_monocular(p_base_url text, p_tsquery_web text)
5+
returns setof json as $$
6+
declare
7+
v_tsquery_web tsquery := websearch_to_tsquery(p_tsquery_web);
8+
begin
9+
return query
10+
with packages_found as (
11+
select
12+
p.normalized_name as package_name,
13+
s.description,
14+
s.version,
15+
s.app_version,
16+
r.name as repository_name,
17+
(case when p_tsquery_web <> '' then
18+
ts_rank(ts_filter(tsdoc, '{a}'), v_tsquery_web, 1) +
19+
ts_rank('{0.1, 0.2, 0.2, 1.0}', ts_filter(tsdoc, '{b,c}'), v_tsquery_web)
20+
else 1 end) as rank
21+
from package p
22+
join snapshot s using (package_id)
23+
join repository r using (repository_id)
24+
where r.repository_kind_id = 0 -- Helm
25+
and s.version = p.latest_version
26+
and (s.deprecated is null or s.deprecated = false)
27+
and
28+
case when p_tsquery_web <> '' then
29+
v_tsquery_web @@ p.tsdoc
30+
else true end
31+
order by rank desc, package_name asc
32+
)
33+
select json_build_object(
34+
'data', (
35+
select coalesce(json_agg(json_build_object(
36+
'id', format('%s/%s', repository_name, package_name),
37+
'artifactHub', json_build_object(
38+
'packageUrl', format(
39+
'%s/packages/helm/%s/%s',
40+
p_base_url,
41+
repository_name,
42+
package_name
43+
)
44+
),
45+
'attributes', json_build_object(
46+
'description', description
47+
),
48+
'relationships', json_build_object(
49+
'latestChartVersion', json_build_object(
50+
'data', json_build_object(
51+
'version', version,
52+
'app_version', app_version
53+
)
54+
)
55+
)
56+
)), '[]')
57+
from packages_found
58+
)
59+
);
60+
end
61+
$$ language plpgsql;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
-- Start transaction and plan tests
2+
begin;
3+
select plan(3);
4+
5+
-- Declare some variables
6+
\set user1ID '00000000-0000-0000-0000-000000000001'
7+
\set repo1ID '00000000-0000-0000-0000-000000000001'
8+
\set package1ID '00000000-0000-0000-0000-000000000001'
9+
10+
-- No packages at this point
11+
select is(
12+
search_packages_monocular('https://artifacthub.io', 'package1')::jsonb,
13+
'{"data": []}'::jsonb,
14+
'TsQueryWeb: package1 | No packages expected'
15+
);
16+
17+
-- Seed some data
18+
insert into "user" (user_id, alias, email) values (:'user1ID', 'user1', '[email protected]');
19+
insert into repository (repository_id, name, display_name, url, repository_kind_id, user_id)
20+
values (:'repo1ID', 'repo1', 'Repo 1', 'https://repo1.com', 0, :'user1ID');
21+
insert into package (
22+
package_id,
23+
name,
24+
latest_version,
25+
tsdoc,
26+
repository_id
27+
) values (
28+
:'package1ID',
29+
'package1',
30+
'1.0.0',
31+
generate_package_tsdoc('package1', null, 'description', '{"kw1", "kw2"}', '{"repo1"}', '{"user1"}'),
32+
:'repo1ID'
33+
);
34+
insert into snapshot (
35+
package_id,
36+
version,
37+
description,
38+
app_version
39+
) values (
40+
:'package1ID',
41+
'1.0.0',
42+
'description',
43+
'12.1.0'
44+
);
45+
46+
-- Run some tests
47+
select is(
48+
search_packages_monocular('https://artifacthub.io', 'package1')::jsonb,
49+
'{
50+
"data": [{
51+
"id": "repo1/package1",
52+
"artifactHub": {
53+
"packageUrl": "https://artifacthub.io/packages/helm/repo1/package1"
54+
},
55+
"attributes": {
56+
"description": "description"
57+
},
58+
"relationships": {
59+
"latestChartVersion": {
60+
"data": {
61+
"version": "1.0.0",
62+
"app_version": "12.1.0"
63+
}
64+
}
65+
}
66+
}]
67+
}'::jsonb,
68+
'TsQueryWeb: package1 | Package1 expected'
69+
);
70+
select is(
71+
search_packages_monocular('https://artifacthub.io', 'package2')::jsonb,
72+
'{"data": []}'::jsonb,
73+
'TsQueryWeb: package2 | No packages expected'
74+
);
75+
76+
-- Finish tests and rollback transaction
77+
select * from finish();
78+
rollback;

database/tests/schema/schema.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-- Start transaction and plan tests
22
begin;
3-
select plan(115);
3+
select plan(116);
44

55
-- Check default_text_search_config is correct
66
select results_eq(
@@ -338,6 +338,7 @@ select has_function('get_packages_stats');
338338
select has_function('get_random_packages');
339339
select has_function('register_package');
340340
select has_function('search_packages');
341+
select has_function('search_packages_monocular');
341342
select has_function('semver_gt');
342343
select has_function('semver_gte');
343344
select has_function('toggle_star');

docs/api/custom-styles.css

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
height: 50px;
1010
}
1111

12+
.topbar-wrapper img {
13+
opacity: 0;
14+
}
15+
1216
@media only screen and (max-width: 575.98px) {
1317
.opblock-section-request-body .opblock-section-header {
1418
flex-direction: column;

docs/api/index.html

+7-4
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,20 @@
4444
<script>
4545
window.onload = function() {
4646
const ui = SwaggerUIBundle({
47-
url: "openapi.yaml",
47+
urls: [
48+
{url: "openapi.yaml", name: "Artifact Hub API"},
49+
{url: "openapi-monocular.yaml", name: "Monocular compatible search API"},
50+
],
4851
dom_id: '#swagger-ui',
4952
deepLinking: true,
5053
presets: [
5154
SwaggerUIBundle.presets.apis,
52-
SwaggerUIStandalonePreset
55+
SwaggerUIStandalonePreset,
5356
],
5457
plugins: [
55-
SwaggerUIBundle.plugins.DownloadUrl
58+
SwaggerUIBundle.plugins.DownloadUrl,
5659
],
57-
layout: "BaseLayout"
60+
layout: "StandaloneLayout"
5861
})
5962
window.ui = ui
6063
}

0 commit comments

Comments
 (0)