Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Monocular compatible search API endpoint #598

Merged
merged 1 commit into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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