)}
- {versions.length === 0 && !isInitialLoading && !isError && (
+ {!isInitialLoading && !isError && (
- No versions available
-
- Portainer is unable to find any versions for this chart in the
- repositories saved. Try adding a new repository which contains
- the chart in the{' '}
-
- Helm repositories settings
-
-
- }
- />
-
- )}
- {isNewVersionAvailable && (
-
- New version available ({latestVersionAvailable})
+ {getStatusMessage(
+ versions.length === 0,
+ latestVersionAvailable,
+ isNewVersionAvailable
+ )}
+ {versions.length === 0 && (
+
+ Portainer is unable to find any versions for this chart in the
+ repositories saved. Try adding a new repository which contains
+ the chart in the{' '}
+
+ Helm repositories settings
+
+
+ }
+ />
+ )}
+
)}
@@ -164,4 +185,18 @@ export function UpgradeButton({
},
});
}
+
+ function getStatusMessage(
+ hasNoAvailableVersions: boolean,
+ latestVersionAvailable: string,
+ isNewVersionAvailable: boolean
+ ): string {
+ if (hasNoAvailableVersions) {
+ return 'No versions available ';
+ }
+ if (isNewVersionAvailable) {
+ return `New version available (${latestVersionAvailable}) `;
+ }
+ return '';
+ }
}
diff --git a/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRepositories.ts b/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRepositories.ts
index 733b79dea..bf11eadb1 100644
--- a/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRepositories.ts
+++ b/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRepositories.ts
@@ -45,20 +45,21 @@ export function useHelmRepositories() {
export function useHelmRepoVersions(
chart: string,
staleTime: number,
- repositories: string[] = []
+ repositories: string[] = [],
+ useCache: boolean = true
) {
// Fetch versions from each repository in parallel as separate queries
const versionQueries = useQueries({
queries: useMemo(
() =>
repositories.map((repo) => ({
- queryKey: ['helm', 'repositories', chart, repo],
- queryFn: () => getSearchHelmRepo(repo, chart),
+ queryKey: ['helm', 'repositories', chart, repo, useCache],
+ queryFn: () => getSearchHelmRepo(repo, chart, useCache),
enabled: !!chart && repositories.length > 0,
staleTime,
...withGlobalError(`Unable to retrieve versions from ${repo}`),
})),
- [repositories, chart, staleTime]
+ [repositories, chart, staleTime, useCache]
),
});
@@ -72,6 +73,8 @@ export function useHelmRepoVersions(
data: allVersions,
isInitialLoading: versionQueries.some((q) => q.isLoading),
isError: versionQueries.some((q) => q.isError),
+ isFetching: versionQueries.some((q) => q.isFetching),
+ refetch: () => Promise.all(versionQueries.map((q) => q.refetch())),
};
}
@@ -80,11 +83,12 @@ export function useHelmRepoVersions(
*/
async function getSearchHelmRepo(
repo: string,
- chart: string
+ chart: string,
+ useCache: boolean = true
): Promise {
try {
const { data } = await axios.get(`templates/helm`, {
- params: { repo, chart },
+ params: { repo, chart, useCache },
});
const versions = data.entries[chart];
return (
diff --git a/pkg/libhelm/options/search_repo_options.go b/pkg/libhelm/options/search_repo_options.go
index 73acc5709..0b35c0bbd 100644
--- a/pkg/libhelm/options/search_repo_options.go
+++ b/pkg/libhelm/options/search_repo_options.go
@@ -3,6 +3,8 @@ package options
import "net/http"
type SearchRepoOptions struct {
- Repo string `example:"https://charts.gitlab.io/"`
- Client *http.Client `example:"&http.Client{Timeout: time.Second * 10}"`
+ Repo string `example:"https://charts.gitlab.io/"`
+ Client *http.Client `example:"&http.Client{Timeout: time.Second * 10}"`
+ Chart string `example:"my-chart"`
+ UseCache bool `example:"false"`
}
diff --git a/pkg/libhelm/sdk/search_repo.go b/pkg/libhelm/sdk/search_repo.go
index 90dd98529..63015330c 100644
--- a/pkg/libhelm/sdk/search_repo.go
+++ b/pkg/libhelm/sdk/search_repo.go
@@ -4,6 +4,8 @@ import (
"net/url"
"os"
"path/filepath"
+ "sync"
+ "time"
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
@@ -25,6 +27,17 @@ type RepoIndex struct {
Generated string `json:"generated"`
}
+type RepoIndexCache struct {
+ Index *repo.IndexFile
+ Timestamp time.Time
+}
+
+var (
+ indexCache = make(map[string]RepoIndexCache)
+ cacheMutex sync.RWMutex
+ cacheDuration = 60 * time.Minute
+)
+
// SearchRepo downloads the `index.yaml` file for specified repo, parses it and returns JSON to caller.
func (hspm *HelmSDKPackageManager) SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error) {
// Validate input options
@@ -53,6 +66,18 @@ func (hspm *HelmSDKPackageManager) SearchRepo(searchRepoOpts options.SearchRepoO
return nil, err
}
+ // Check cache first
+ if searchRepoOpts.UseCache {
+ cacheMutex.RLock()
+ if cached, exists := indexCache[repoURL.String()]; exists {
+ if time.Since(cached.Timestamp) < cacheDuration {
+ cacheMutex.RUnlock()
+ return convertAndMarshalIndex(cached.Index, searchRepoOpts.Chart)
+ }
+ }
+ cacheMutex.RUnlock()
+ }
+
// Set up Helm CLI environment
repoSettings := cli.New()
@@ -92,23 +117,21 @@ func (hspm *HelmSDKPackageManager) SearchRepo(searchRepoOpts options.SearchRepoO
return nil, err
}
- // Convert the index file to our response format
- result, err := convertIndexToResponse(indexFile)
- if err != nil {
- log.Error().
- Str("context", "HelmClient").
- Err(err).
- Msg("Failed to convert index to response format")
- return nil, errors.Wrap(err, "failed to convert index to response format")
+ // Update cache and remove old entries
+ cacheMutex.Lock()
+ indexCache[searchRepoOpts.Repo] = RepoIndexCache{
+ Index: indexFile,
+ Timestamp: time.Now(),
+ }
+ for key, index := range indexCache {
+ if time.Since(index.Timestamp) > cacheDuration {
+ delete(indexCache, key)
+ }
}
- log.Debug().
- Str("context", "HelmClient").
- Str("repo", searchRepoOpts.Repo).
- Int("entries_count", len(indexFile.Entries)).
- Msg("Successfully searched repository")
+ cacheMutex.Unlock()
- return json.Marshal(result)
+ return convertAndMarshalIndex(indexFile, searchRepoOpts.Chart)
}
// validateSearchRepoOptions validates the required search repository options.
@@ -216,7 +239,7 @@ func loadIndexFile(indexPath string) (*repo.IndexFile, error) {
}
// convertIndexToResponse converts the Helm index file to our response format.
-func convertIndexToResponse(indexFile *repo.IndexFile) (RepoIndex, error) {
+func convertIndexToResponse(indexFile *repo.IndexFile, chartName string) (RepoIndex, error) {
result := RepoIndex{
APIVersion: indexFile.APIVersion,
Entries: make(map[string][]ChartInfo),
@@ -225,7 +248,9 @@ func convertIndexToResponse(indexFile *repo.IndexFile) (RepoIndex, error) {
// Convert Helm SDK types to our response types
for name, charts := range indexFile.Entries {
- result.Entries[name] = convertChartsToChartInfo(charts)
+ if chartName == "" || name == chartName {
+ result.Entries[name] = convertChartsToChartInfo(charts)
+ }
}
return result, nil
@@ -349,3 +374,23 @@ func ensureHelmDirectoriesExist(settings *cli.EnvSettings) error {
return nil
}
+
+func convertAndMarshalIndex(indexFile *repo.IndexFile, chartName string) ([]byte, error) {
+ // Convert the index file to our response format
+ result, err := convertIndexToResponse(indexFile, chartName)
+ if err != nil {
+ log.Error().
+ Str("context", "HelmClient").
+ Err(err).
+ Msg("Failed to convert index to response format")
+ return nil, errors.Wrap(err, "failed to convert index to response format")
+ }
+
+ log.Debug().
+ Str("context", "HelmClient").
+ Str("repo", chartName).
+ Int("entries_count", len(indexFile.Entries)).
+ Msg("Successfully searched repository")
+
+ return json.Marshal(result)
+}