Query ArtifactHub for repo suggestion (#225)

* Query ArtifactHub for repo suggestion

* Refactor & improve

* Add notice on local chart support
This commit is contained in:
Andrey Pokhilko
2023-03-02 10:22:32 +00:00
committed by GitHub
parent 679d31e4ab
commit bbb425bfea
7 changed files with 182 additions and 3 deletions

View File

@@ -34,6 +34,11 @@ type options struct {
} }
func main() { func main() {
err := os.Setenv("HD_VERSION", version) // for anyone willing to access it
if err != nil {
fmt.Println("Failed to remember app version because of error: " + err.Error())
}
opts := parseFlags() opts := parseFlags()
if opts.BindHost == "" { if opts.BindHost == "" {
host := os.Getenv("HD_BIND") host := os.Getenv("HD_BIND")

View File

@@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -207,7 +208,21 @@ func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
if len(res) > 0 { if len(res) > 0 {
c.IndentedJSON(http.StatusOK, res[:1]) c.IndentedJSON(http.StatusOK, res[:1])
} else { } else {
// caching it to avoid too many requests
found, err := h.Data.Cache.String("chart-artifacthub-query/"+qp.Name, nil, func() (string, error) {
return h.repoFromArtifactHub(qp.Name)
})
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
if found == "" {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} else {
c.Header("Content-Type", "application/json")
c.String(http.StatusOK, found)
}
} }
} }
@@ -553,6 +568,49 @@ func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDi
return res, nil return res, nil
} }
func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
results, err := objects.QueryArtifactHub(name)
if err != nil {
log.Warnf("Failed to query ArtifactHub: %s", err)
return "", nil // swallowing the error to not annoy users
}
if len(results) == 0 {
return "", nil
}
sort.SliceStable(results, func(i, j int) bool {
// we prefer official repos
if results[i].Repository.Official && !results[j].Repository.Official {
return true
}
// or from verified publishers
if results[i].Repository.VerifiedPublisher && !results[j].Repository.VerifiedPublisher {
return true
}
// or just more popular
return results[i].Stars > results[j].Stars
})
r := results[0]
buf, err := json.Marshal([]*RepoChartElement{{
Name: r.Name,
Version: r.Version,
AppVersion: r.AppVersion,
Description: r.Description,
Repository: r.Repository.Name,
URLs: []string{r.Repository.Url},
IsSuggestedRepo: true,
}})
if err != nil {
return "", err
}
return string(buf), nil
}
type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
@@ -563,6 +621,7 @@ type RepoChartElement struct { // TODO: do we need it at all? there is existing
InstalledName string `json:"installed_name"` InstalledName string `json:"installed_name"`
Repository string `json:"repository"` Repository string `json:"repository"`
URLs []string `json:"urls"` URLs []string `json:"urls"`
IsSuggestedRepo bool `json:"isSuggestedRepo"`
} }
func HReleaseToJSON(o *release.Release) *ReleaseElement { func HReleaseToJSON(o *release.Release) *ReleaseElement {

View File

@@ -0,0 +1,90 @@
package objects
import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
"net/http"
neturl "net/url"
"os"
"sync"
)
var mxArtifactHub sync.Mutex
func QueryArtifactHub(chartName string) ([]*ArtifactHubResult, error) {
mxArtifactHub.Lock() // to avoid parallel request spike
defer mxArtifactHub.Unlock()
url := os.Getenv("HD_ARTIFACT_HUB_URL")
if url == "" {
url = "https://artifacthub.io/api/v1/packages/search"
}
p, err := neturl.Parse(url)
if err != nil {
return nil, err
}
p.RawQuery = "offset=0&limit=5&facets=false&kind=0&deprecated=false&sort=relevance&ts_query_web=" + neturl.QueryEscape(chartName)
req, err := http.NewRequest("GET", p.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Komodor Helm Dashboard/"+os.Getenv("HD_VERSION")) // TODO
log.Debugf("Making HTTP request: %v", req)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status)
}
result := ArtifactHubResults{}
err = json.NewDecoder(res.Body).Decode(&result)
if err != nil {
return nil, err
}
return result.Packages, nil
}
type ArtifactHubResults struct {
Packages []*ArtifactHubResult `json:"packages"`
}
type ArtifactHubResult struct {
PackageId string `json:"package_id"`
Name string `json:"name"`
NormalizedName string `json:"normalized_name"`
LogoImageId string `json:"logo_image_id"`
Stars int `json:"stars"`
Description string `json:"description"`
Version string `json:"version"`
AppVersion string `json:"app_version"`
Deprecated bool `json:"deprecated"`
Signed bool `json:"signed"`
ProductionOrganizationsCount int `json:"production_organizations_count"`
Ts int `json:"ts"`
Repository ArtifactHubRepo `json:"repository"`
}
type ArtifactHubRepo struct {
Url string `json:"url"`
Kind int `json:"kind"`
Name string `json:"name"`
Official bool `json:"official"`
DisplayName string `json:"display_name"`
RepositoryId string `json:"repository_id"`
ScannerDisabled bool `json:"scanner_disabled"`
OrganizationName string `json:"organization_name"`
VerifiedPublisher bool `json:"verified_publisher"`
OrganizationDisplayName string `json:"organization_display_name"`
}

View File

@@ -25,7 +25,11 @@ function checkUpgradeable(name) {
if (!data || !data.length) { if (!data || !data.length) {
btnUpgradeCheck.prop("disabled", true) btnUpgradeCheck.prop("disabled", true)
btnUpgradeCheck.text("") btnUpgradeCheck.text("")
$("#btnAddRepository").text("Add repository for it") $("#btnAddRepository").text("Add repository for it").data("suggestRepo", "")
} else if (data[0].isSuggestedRepo) {
btnUpgradeCheck.prop("disabled", true)
btnUpgradeCheck.text("")
$("#btnAddRepository").text("Add repository for it: "+data[0].repository).data("suggestRepo", data[0].repository).data("suggestRepoUrl", data[0].urls[0])
} else { } else {
$("#btnAddRepository").text("") $("#btnAddRepository").text("")
btnUpgradeCheck.text("Check for new version") btnUpgradeCheck.text("Check for new version")
@@ -399,7 +403,12 @@ $("#btnRollback").click(function () {
}) })
$("#btnAddRepository").click(function () { $("#btnAddRepository").click(function () {
const self=$(this)
setHashParam("section", "repository") setHashParam("section", "repository")
if (self.data("suggestRepo")) {
setHashParam("suggestRepo", self.data("suggestRepo"))
setHashParam("suggestRepoUrl", self.data("suggestRepoUrl"))
}
window.location.reload() window.location.reload()
}) })

View File

@@ -89,6 +89,7 @@
<button class="btn btn-sm border-secondary text-muted"> <button class="btn btn-sm border-secondary text-muted">
<i class="bi-plus-lg"></i> Add Repository <i class="bi-plus-lg"></i> Add Repository
</button> </button>
<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>
</div> </div>
</div> </div>
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded"> <div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">

View File

@@ -89,7 +89,13 @@ function buildChartCard(elm) {
} }
if (isNewerVersion(elm.chartVersion, data[0].version)) { if (isNewerVersion(elm.chartVersion, data[0].version)) {
card.find(".rel-name span").append("<span class='bi-arrow-up-circle-fill ms-2 text-success' title='Upgrade available: "+data[0].version+"'></span>") const icon = $("<span class='ms-2 text-success' title='Upgrade available: " + data[0].version + " from " + data[0].repository + "'></span>")
if (data[0].isSuggestedRepo) {
icon.addClass("bi-arrow-up-circle")
} else {
icon.addClass("bi-arrow-up-circle-fill")
}
card.find(".rel-name span").append(icon)
} }
}) })

View File

@@ -2,6 +2,13 @@ function loadRepoView() {
$("#sectionRepo .repo-details").hide() $("#sectionRepo .repo-details").hide()
$("#sectionRepo").show() $("#sectionRepo").show()
$("#repoAddModal input[name=name]").val(getHashParam("suggestRepo"))
$("#repoAddModal input[name=url]").val(getHashParam("suggestRepoUrl"))
if (getHashParam("suggestRepo")) {
$("#sectionRepo .repo-list .btn").click()
}
$.getJSON("/api/helm/repositories").fail(function (xhr) { $.getJSON("/api/helm/repositories").fail(function (xhr) {
reportError("Failed to get list of repositories", xhr) reportError("Failed to get list of repositories", xhr)
sendStats('Get repo', {'status': 'fail'}); sendStats('Get repo', {'status': 'fail'});
@@ -85,6 +92,8 @@ $("#inputSearch").keyup(function () {
}) })
$("#sectionRepo .repo-list .btn").click(function () { $("#sectionRepo .repo-list .btn").click(function () {
setHashParam("suggestRepo", null)
setHashParam("suggestRepoUrl", null)
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {}); const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
myModal.show() myModal.show()
}) })