Skip to content

Commit bbb425b

Browse files
authored
Query ArtifactHub for repo suggestion (#225)
* Query ArtifactHub for repo suggestion * Refactor & improve * Add notice on local chart support
1 parent 679d31e commit bbb425b

File tree

7 files changed

+182
-3
lines changed

7 files changed

+182
-3
lines changed

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type options struct {
3434
}
3535

3636
func main() {
37+
err := os.Setenv("HD_VERSION", version) // for anyone willing to access it
38+
if err != nil {
39+
fmt.Println("Failed to remember app version because of error: " + err.Error())
40+
}
41+
3742
opts := parseFlags()
3843
if opts.BindHost == "" {
3944
host := os.Getenv("HD_BIND")

pkg/dashboard/handlers/helmHandlers.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"github.com/gin-gonic/gin"
@@ -207,7 +208,21 @@ func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
207208
if len(res) > 0 {
208209
c.IndentedJSON(http.StatusOK, res[:1])
209210
} else {
210-
c.Status(http.StatusNoContent)
211+
// caching it to avoid too many requests
212+
found, err := h.Data.Cache.String("chart-artifacthub-query/"+qp.Name, nil, func() (string, error) {
213+
return h.repoFromArtifactHub(qp.Name)
214+
})
215+
if err != nil {
216+
_ = c.AbortWithError(http.StatusInternalServerError, err)
217+
return
218+
}
219+
220+
if found == "" {
221+
c.Status(http.StatusNoContent)
222+
} else {
223+
c.Header("Content-Type", "application/json")
224+
c.String(http.StatusOK, found)
225+
}
211226
}
212227
}
213228

@@ -553,6 +568,49 @@ func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDi
553568
return res, nil
554569
}
555570

571+
func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
572+
results, err := objects.QueryArtifactHub(name)
573+
if err != nil {
574+
log.Warnf("Failed to query ArtifactHub: %s", err)
575+
return "", nil // swallowing the error to not annoy users
576+
}
577+
578+
if len(results) == 0 {
579+
return "", nil
580+
}
581+
582+
sort.SliceStable(results, func(i, j int) bool {
583+
// we prefer official repos
584+
if results[i].Repository.Official && !results[j].Repository.Official {
585+
return true
586+
}
587+
588+
// or from verified publishers
589+
if results[i].Repository.VerifiedPublisher && !results[j].Repository.VerifiedPublisher {
590+
return true
591+
}
592+
593+
// or just more popular
594+
return results[i].Stars > results[j].Stars
595+
})
596+
597+
r := results[0]
598+
buf, err := json.Marshal([]*RepoChartElement{{
599+
Name: r.Name,
600+
Version: r.Version,
601+
AppVersion: r.AppVersion,
602+
Description: r.Description,
603+
Repository: r.Repository.Name,
604+
URLs: []string{r.Repository.Url},
605+
IsSuggestedRepo: true,
606+
}})
607+
if err != nil {
608+
return "", err
609+
}
610+
611+
return string(buf), nil
612+
}
613+
556614
type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm
557615
Name string `json:"name"`
558616
Version string `json:"version"`
@@ -563,6 +621,7 @@ type RepoChartElement struct { // TODO: do we need it at all? there is existing
563621
InstalledName string `json:"installed_name"`
564622
Repository string `json:"repository"`
565623
URLs []string `json:"urls"`
624+
IsSuggestedRepo bool `json:"isSuggestedRepo"`
566625
}
567626

568627
func HReleaseToJSON(o *release.Release) *ReleaseElement {

pkg/dashboard/objects/artifacthub.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package objects
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
log "github.com/sirupsen/logrus"
7+
"net/http"
8+
neturl "net/url"
9+
"os"
10+
"sync"
11+
)
12+
13+
var mxArtifactHub sync.Mutex
14+
15+
func QueryArtifactHub(chartName string) ([]*ArtifactHubResult, error) {
16+
mxArtifactHub.Lock() // to avoid parallel request spike
17+
defer mxArtifactHub.Unlock()
18+
19+
url := os.Getenv("HD_ARTIFACT_HUB_URL")
20+
if url == "" {
21+
url = "https://artifacthub.io/api/v1/packages/search"
22+
}
23+
24+
p, err := neturl.Parse(url)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
p.RawQuery = "offset=0&limit=5&facets=false&kind=0&deprecated=false&sort=relevance&ts_query_web=" + neturl.QueryEscape(chartName)
30+
31+
req, err := http.NewRequest("GET", p.String(), nil)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
req.Header.Set("User-Agent", "Komodor Helm Dashboard/"+os.Getenv("HD_VERSION")) // TODO
37+
38+
log.Debugf("Making HTTP request: %v", req)
39+
res, err := http.DefaultClient.Do(req)
40+
if err != nil {
41+
return nil, err
42+
}
43+
defer res.Body.Close()
44+
45+
if res.StatusCode != 200 {
46+
return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status)
47+
}
48+
49+
result := ArtifactHubResults{}
50+
51+
err = json.NewDecoder(res.Body).Decode(&result)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
return result.Packages, nil
57+
}
58+
59+
type ArtifactHubResults struct {
60+
Packages []*ArtifactHubResult `json:"packages"`
61+
}
62+
63+
type ArtifactHubResult struct {
64+
PackageId string `json:"package_id"`
65+
Name string `json:"name"`
66+
NormalizedName string `json:"normalized_name"`
67+
LogoImageId string `json:"logo_image_id"`
68+
Stars int `json:"stars"`
69+
Description string `json:"description"`
70+
Version string `json:"version"`
71+
AppVersion string `json:"app_version"`
72+
Deprecated bool `json:"deprecated"`
73+
Signed bool `json:"signed"`
74+
ProductionOrganizationsCount int `json:"production_organizations_count"`
75+
Ts int `json:"ts"`
76+
Repository ArtifactHubRepo `json:"repository"`
77+
}
78+
79+
type ArtifactHubRepo struct {
80+
Url string `json:"url"`
81+
Kind int `json:"kind"`
82+
Name string `json:"name"`
83+
Official bool `json:"official"`
84+
DisplayName string `json:"display_name"`
85+
RepositoryId string `json:"repository_id"`
86+
ScannerDisabled bool `json:"scanner_disabled"`
87+
OrganizationName string `json:"organization_name"`
88+
VerifiedPublisher bool `json:"verified_publisher"`
89+
OrganizationDisplayName string `json:"organization_display_name"`
90+
}

pkg/dashboard/static/actions.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ function checkUpgradeable(name) {
2525
if (!data || !data.length) {
2626
btnUpgradeCheck.prop("disabled", true)
2727
btnUpgradeCheck.text("")
28-
$("#btnAddRepository").text("Add repository for it")
28+
$("#btnAddRepository").text("Add repository for it").data("suggestRepo", "")
29+
} else if (data[0].isSuggestedRepo) {
30+
btnUpgradeCheck.prop("disabled", true)
31+
btnUpgradeCheck.text("")
32+
$("#btnAddRepository").text("Add repository for it: "+data[0].repository).data("suggestRepo", data[0].repository).data("suggestRepoUrl", data[0].urls[0])
2933
} else {
3034
$("#btnAddRepository").text("")
3135
btnUpgradeCheck.text("Check for new version")
@@ -399,7 +403,12 @@ $("#btnRollback").click(function () {
399403
})
400404

401405
$("#btnAddRepository").click(function () {
406+
const self=$(this)
402407
setHashParam("section", "repository")
408+
if (self.data("suggestRepo")) {
409+
setHashParam("suggestRepo", self.data("suggestRepo"))
410+
setHashParam("suggestRepoUrl", self.data("suggestRepoUrl"))
411+
}
403412
window.location.reload()
404413
})
405414

pkg/dashboard/static/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ <h4 class="fs-6">Repositories</h4>
8989
<button class="btn btn-sm border-secondary text-muted">
9090
<i class="bi-plus-lg"></i> Add Repository
9191
</button>
92+
<div class="mt-2 p-2 small">Charts developers: you can also add local directories as chart source. Use <span class="font-monospace text-success">--local-chart</span> CLI switch to specify it.</div>
9293
</div>
9394
</div>
9495
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">

pkg/dashboard/static/list-view.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,13 @@ function buildChartCard(elm) {
8989
}
9090

9191
if (isNewerVersion(elm.chartVersion, data[0].version)) {
92-
card.find(".rel-name span").append("<span class='bi-arrow-up-circle-fill ms-2 text-success' title='Upgrade available: "+data[0].version+"'></span>")
92+
const icon = $("<span class='ms-2 text-success' title='Upgrade available: " + data[0].version + " from " + data[0].repository + "'></span>")
93+
if (data[0].isSuggestedRepo) {
94+
icon.addClass("bi-arrow-up-circle")
95+
} else {
96+
icon.addClass("bi-arrow-up-circle-fill")
97+
}
98+
card.find(".rel-name span").append(icon)
9399
}
94100
})
95101

pkg/dashboard/static/repo.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ function loadRepoView() {
22
$("#sectionRepo .repo-details").hide()
33
$("#sectionRepo").show()
44

5+
$("#repoAddModal input[name=name]").val(getHashParam("suggestRepo"))
6+
$("#repoAddModal input[name=url]").val(getHashParam("suggestRepoUrl"))
7+
8+
if (getHashParam("suggestRepo")) {
9+
$("#sectionRepo .repo-list .btn").click()
10+
}
11+
512
$.getJSON("/api/helm/repositories").fail(function (xhr) {
613
reportError("Failed to get list of repositories", xhr)
714
sendStats('Get repo', {'status': 'fail'});
@@ -85,6 +92,8 @@ $("#inputSearch").keyup(function () {
8592
})
8693

8794
$("#sectionRepo .repo-list .btn").click(function () {
95+
setHashParam("suggestRepo", null)
96+
setHashParam("suggestRepoUrl", null)
8897
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
8998
myModal.show()
9099
})

0 commit comments

Comments
 (0)