7 Commits

Author SHA1 Message Date
Andrei Pohilko
37e1d44bf1 Remove survey banner code 2023-03-07 15:07:59 +00:00
Andrei Pohilko
362cb09e6d Improve logo source 2023-03-07 15:00:49 +00:00
Andrei Pohilko
209f5b5e44 Improve upgradable status display 2023-03-07 14:51:46 +00:00
Andrei Pohilko
a0680a4820 Add links to Komodor 2023-03-07 12:51:39 +00:00
Andrei Pohilko
d95cac94d5 Auto-update repositories each 10 minutes, unless HD_NO_AUTOUPDATE is set 2023-03-06 11:38:33 +00:00
Andrey Pokhilko
bbb425bfea Query ArtifactHub for repo suggestion (#225)
* Query ArtifactHub for repo suggestion

* Refactor & improve

* Add notice on local chart support
2023-03-02 10:22:32 +00:00
komodor-bot
679d31e4ab Increment chart versions [skip ci] 2023-02-22 12:45:52 +00:00
12 changed files with 241 additions and 65 deletions

View File

@@ -5,5 +5,5 @@ name: helm-dashboard
description: A GUI Dashboard for Helm by Komodor description: A GUI Dashboard for Helm by Komodor
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg" icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg"
version: 0.1.4 version: 0.1.5
appVersion: "1.1.0" appVersion: "1.1.1"

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 {
c.Status(http.StatusNoContent) // 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)
} else {
c.Header("Content-Type", "application/json")
c.String(http.StatusOK, found)
}
} }
} }
@@ -235,7 +250,6 @@ func (h *HelmHandler) RepoCharts(c *gin.Context) {
return return
} }
// TODO: enrich with installed
enrichRepoChartsWithInstalled(charts, installed) enrichRepoChartsWithInstalled(charts, installed)
sort.Slice(charts, func(i, j int) bool { sort.Slice(charts, func(i, j int) bool {
@@ -553,6 +567,61 @@ 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 {
ri, rj := results[i], results[j]
// we prefer official repos
if ri.Repository.Official && !rj.Repository.Official {
return true
}
// or from verified publishers
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
return true
}
// or just more popular
if ri.Stars > rj.Stars {
return true
}
// or with more recent app version
if semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion) > 0 {
return true
}
return false
})
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 +632,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

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"os"
"sync" "sync"
"time" "time"
@@ -194,13 +195,12 @@ func (d *DataLayer) nsForCtx(ctx string) string {
} }
func (d *DataLayer) PeriodicTasks(ctx context.Context) { func (d *DataLayer) PeriodicTasks(ctx context.Context) {
if !d.StatusInfo.ClusterMode { // TODO: maybe have a separate flag for that? // TODO: separate scanning setup for in-cluster?
log.Debugf("Not in cluster mode, not starting background tasks")
return
}
// auto-update repos if os.Getenv("HD_NO_AUTOUPDATE") == "" {
go d.loopUpdateRepos(ctx, 10*time.Minute) // TODO: parameterize interval? // auto-update repos
go d.loopUpdateRepos(ctx, 10*time.Minute) // TODO: parameterize interval?
}
// auto-scan // auto-scan
} }

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

@@ -196,7 +196,7 @@ function showResources(namespace, chart, revision) {
const statusBlock = resBlock.find(".res-status"); const statusBlock = resBlock.find(".res-status");
statusBlock.empty().append(badge).attr("title", data.status.phase) statusBlock.empty().append(badge).attr("title", data.status.phase)
const statusMessage = getStatusMessage(data.status) const statusMessage = getStatusMessage(data.status)
resBlock.find(".res-statusmsg").html("<span class='text-muted small'>" + (statusMessage ? statusMessage : '') + "</span>") resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>")
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) { if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
resBlock.find(".res-actions") resBlock.find(".res-actions")
@@ -215,6 +215,10 @@ function showResources(namespace, chart, revision) {
}) })
} }
} }
if (badge.hasClass("bg-danger")) {
resBlock.find(".res-statusmsg").append("<a href='" + KomodorCTALink + "' class='btn btn-primary btn-sm fw-normal fs-80' target='_blank'>Troubleshoot in Komodor <i class='bi-box-arrow-up-right'></i></a>")
}
}) })
} }
}) })

View File

@@ -69,9 +69,15 @@
</a></li> </a></li>
</ul> </ul>
<div> <div>
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img <div class="border-muted text-muted border rounded p-1 pe-2 me-3 d-flex">
src="static/komodor-logo.svg" alt="komodor.io" <img alt="Komodor" src="https://raw.githubusercontent.com/komodorio/helm-charts/master/k8s-watcher.svg" class="me-2" style="width: 42px; height: 42px"/>
style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a> <span class="text-nowrap">
<a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
class="link text-primary fw-bold text-decoration-none">Upgrade your HELM experience - Free
<i class="bi-box-arrow-up-right ms-1"></i></a><br/>
Auth & RBAC, k8s events, troubleshooting and more
</span>
</div>
</div> </div>
<div class="separator-vertical"><span></span></div> <div class="separator-vertical"><span></span></div>
<i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i> <i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
@@ -89,6 +95,9 @@
<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">
@@ -304,6 +313,8 @@
<hr> <hr>
<p style="white-space: pre-wrap"></p> <p style="white-space: pre-wrap"></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<hr/>
<span class="small text-muted fs-80">Hint: Komodor has the same HELM capabilities, with enterprise features and support. <a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash" target="_blank">Sign up for free.</a></span>
</div> </div>
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal" <div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal"
@@ -313,8 +324,12 @@
<h5 id="describeModalLabel"></h5> <h5 id="describeModalLabel"></h5>
<p class="m-0 mt-4">ResourceType</p> <p class="m-0 mt-4">ResourceType</p>
</div> </div>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button> <div>
<a href='https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash'
class='btn btn-primary btn-sm me-2' target='_blank'>See more details in Komodor <i
class='bi-box-arrow-up-right'></i></a>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
</div> </div>
<div class="offcanvas-body p-2 ps-4" id="describeModalBody"> <div class="offcanvas-body p-2 ps-4" id="describeModalBody">
</div> </div>
@@ -478,47 +493,5 @@
<script src="static/actions.js"></script> <script src="static/actions.js"></script>
<script src="static/scripts.js"></script> <script src="static/scripts.js"></script>
<!-- BANNER START
<a id="banner"
href="https://helm-dashboard-survey.komodor.com/"
class="display-none position-absolute top-0 start-50 translate-middle-x bg-primary text-light rounded px-2 mt-1 text-decoration-none py-1">Help
shaping the future by participating in user survey <b class="bi-x-lg"></b></a>
<script>
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
const c = ca[i].trim();
if (c.indexOf(nameEQ) === 0) {
return c.substring(nameEQ.length, c.length)
}
}
return null;
}
const cookie = getCookie("hideBanner");
if (cookie == null) {
console.log("show")
$("#banner").show()
}
$("#banner b").click(function (evt) {
evt.preventDefault()
setCookie("hideBanner", "1", 365);
$("#banner").hide()
})
</script>
/BANNER END -->
</body> </body>
</html> </html>

View File

@@ -88,8 +88,22 @@ function buildChartCard(elm) {
return return
} }
if (isNewerVersion(elm.chartVersion, data[0].version)) { if (isNewerVersion(elm.chartVersion, data[0].version) || data[0].isSuggestedRepo) {
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 = $("<br/><span class='ms-2 fw-bold' data-bs-toggle='tooltip' data-bs-placement='bottom'></span>")
if (data[0].isSuggestedRepo) {
icon.addClass("bi-plus-circle-fill text-primary")
icon.text(" ADD REPO")
icon.attr("data-bs-title", "Add '" + data[0].repository+"' to list of known repositories")
} else {
icon.addClass("bi-arrow-up-circle-fill text-primary")
icon.text(" UPGRADE")
icon.attr("data-bs-title", "Upgrade available: " + data[0].version + " from " + data[0].repository)
}
card.find(".rel-chart div").append(icon)
const tooltipTriggerList = card.find('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
sendStats('upgradeIconShown', {'isProbable': data[0].isSuggestedRepo})
} }
}) })

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()
}) })

View File

@@ -117,8 +117,8 @@ $("#topNav ul a").click(function () {
initView() initView()
}) })
const myAlert = document.getElementById('errorAlert') const errAlert = document.getElementById('errorAlert')
myAlert.addEventListener('close.bs.alert', event => { errAlert.addEventListener('close.bs.alert', event => {
event.preventDefault() event.preventDefault()
$("#errorAlert").hide() $("#errorAlert").hide()
}) })
@@ -356,4 +356,6 @@ function setFilteredNamespaces(filteredNamespaces) {
} else if (filteredNamespaces.length !== 0) { } else if (filteredNamespaces.length !== 0) {
setHashParam("filteredNamespace", filteredNamespaces.join('+')) setHashParam("filteredNamespace", filteredNamespaces.join('+'))
} }
} }
const KomodorCTALink="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"

View File

@@ -1,5 +1,5 @@
name: "dashboard" name: "dashboard"
version: "1.1.0" version: "1.1.1"
usage: "A simplified way of working with Helm" usage: "A simplified way of working with Helm"
description: "View HELM situation in nice web UI" description: "View HELM situation in nice web UI"
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard" command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"