From 0a7c3e6444ced6f996b8cedbbbb62cc7112933ad Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Tue, 14 Jul 2026 16:45:29 +0200 Subject: [PATCH 1/8] HELM-586: Show toast notification for invalid Helm Chart repositories When a Helm Chart repository has incorrect basicAuth credentials or is unreachable, the backend already marks it in the index.yaml response via a console-warning annotation, but the frontend never read it. Backend: Improve error messages to distinguish auth failures (401/403) from other errors, and include the error reason in the annotation. Frontend: Read the console-warning annotation from the index.yaml response and display it as a toast notification on the catalog page. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Martin Szuc --- .../src/catalog/providers/useHelmCharts.tsx | 24 ++++++++++++++++--- pkg/helm/chartproxy/proxy.go | 2 +- pkg/helm/chartproxy/repos.go | 7 ++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx index 9aad63566dc..1e21cd14df7 100644 --- a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx +++ b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx @@ -1,10 +1,12 @@ -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useRef } from 'react'; +import { AlertVariant } from '@patternfly/react-core'; import { safeLoad } from 'js-yaml'; import { useTranslation } from 'react-i18next'; import type { ExtensionHook, CatalogItem, WatchK8sResults } from '@console/dynamic-plugin-sdk'; import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { referenceForModel } from '@console/internal/module/k8s'; +import { useToast } from '@console/shared/src/components/toast/useToast'; import type { APIError } from '@console/shared/src/types/resource'; import { coFetch } from '@console/shared/src/utils/console-fetch'; import { HelmChartRepositoryModel, ProjectHelmChartRepositoryModel } from '../../models/helm'; @@ -19,8 +21,10 @@ const useHelmCharts: ExtensionHook = ({ namespace, }): [CatalogItem[], boolean, any] => { const { t } = useTranslation('helm-plugin'); + const toast = useToast(); const [helmCharts, setHelmCharts] = useState(); const [loadedError, setLoadedError] = useState(); + const shownWarningRef = useRef(); const resourceSelector = useMemo( () => ({ @@ -52,8 +56,22 @@ const useHelmCharts: ExtensionHook = ({ .then(async (res) => { if (mounted) { const yaml = await res.text(); - const json = safeLoad(yaml) as { entries: HelmChartEntries }; + const json = safeLoad(yaml) as { + entries: HelmChartEntries; + annotations?: Record; + }; setHelmCharts(json.entries); + const warning = json.annotations?.['console-warning']; + if (warning && warning !== shownWarningRef.current) { + shownWarningRef.current = warning; + toast.addToast({ + variant: AlertVariant.warning, + title: t('Helm Chart repository error'), + content: warning, + dismissible: true, + timeout: true, + }); + } } }) .catch((err) => { @@ -65,7 +83,7 @@ const useHelmCharts: ExtensionHook = ({ return () => { mounted = false; }; - }, [namespaceParam]); + }, [namespaceParam, toast, t]); const normalizedHelmCharts: CatalogItem[] = useMemo( () => diff --git a/pkg/helm/chartproxy/proxy.go b/pkg/helm/chartproxy/proxy.go index 22faf42320f..52747605522 100644 --- a/pkg/helm/chartproxy/proxy.go +++ b/pkg/helm/chartproxy/proxy.go @@ -88,7 +88,7 @@ func (p *proxy) IndexFile(onlyCompatible bool, namespace string) (*repo.IndexFil if !helmRepo.Disabled { idxFile, err := helmRepo.IndexFile() if err != nil { - invalidRepos = append(invalidRepos, helmRepo.Name) + invalidRepos = append(invalidRepos, helmRepo.Name+" ("+err.Error()+")") klog.Errorf("Error retrieving index file for %v: %v", helmRepo, err) continue } diff --git a/pkg/helm/chartproxy/repos.go b/pkg/helm/chartproxy/repos.go index 76e1287d9ed..1ade66436bc 100644 --- a/pkg/helm/chartproxy/repos.go +++ b/pkg/helm/chartproxy/repos.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" "crypto/x509" - "errors" "fmt" "io" "net/http" @@ -84,8 +83,12 @@ func (hr helmRepo) IndexFile() (*repo.IndexFile, error) { if err != nil { return nil, err } + defer resp.Body.Close() + if resp.StatusCode == 401 || resp.StatusCode == 403 { + return nil, fmt.Errorf("authentication failed (HTTP %d)", resp.StatusCode) + } if resp.StatusCode != 200 { - return nil, errors.New(fmt.Sprintf("Response for %v returned %v with status code %v", indexURL, resp, resp.StatusCode)) + return nil, fmt.Errorf("failed to fetch index (HTTP %d)", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { From e6dd8bbfee1930bcd9f5293177e521ed66eee71f Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Tue, 14 Jul 2026 18:20:27 +0200 Subject: [PATCH 2/8] HELM-586: Send structured JSON in console-warning annotation for i18n support Surface both fetch-level errors (auth failures, timeouts) and config-level errors (missing secret keys, missing ConfigMaps, HTTPS required) in the console-warning annotation. Previously config errors were silently logged and the repo was dropped without user notification. Change List() to return config errors alongside valid repos so IndexFile() can merge them into the annotation. Use AlertVariant.danger (red) for the toast. Fix newline join to comma-space for proper HTML rendering. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Szuc --- .../src/catalog/providers/useHelmCharts.tsx | 30 ++++++++++----- pkg/helm/actions/utility.go | 2 +- pkg/helm/chartproxy/proxy.go | 17 +++++---- pkg/helm/chartproxy/repos.go | 21 ++++++++--- pkg/helm/chartproxy/repos_test.go | 37 +++++++++++++------ 5 files changed, 72 insertions(+), 35 deletions(-) diff --git a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx index 1e21cd14df7..86a9538bda6 100644 --- a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx +++ b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx @@ -61,16 +61,26 @@ const useHelmCharts: ExtensionHook = ({ annotations?: Record; }; setHelmCharts(json.entries); - const warning = json.annotations?.['console-warning']; - if (warning && warning !== shownWarningRef.current) { - shownWarningRef.current = warning; - toast.addToast({ - variant: AlertVariant.warning, - title: t('Helm Chart repository error'), - content: warning, - dismissible: true, - timeout: true, - }); + const warningData = json.annotations?.['console-warning']; + if (warningData && warningData !== shownWarningRef.current) { + shownWarningRef.current = warningData; + try { + const repos: { name: string; error: string }[] = JSON.parse(warningData); + const repoList = repos + .map((r) => t('{{name}}: {{error}}', { name: r.name, error: r.error })) + .join(', '); + toast.addToast({ + variant: AlertVariant.danger, + title: t('Helm Chart repository error'), + content: t('The following repositories are unreachable: {{repoList}}', { + repoList, + }), + dismissible: true, + timeout: true, + }); + } catch { + // ignore malformed annotation + } } } }) diff --git a/pkg/helm/actions/utility.go b/pkg/helm/actions/utility.go index 91df5a82d69..18bb9a25e28 100644 --- a/pkg/helm/actions/utility.go +++ b/pkg/helm/actions/utility.go @@ -105,7 +105,7 @@ func getChartInfoFromChartUrl( client dynamic.Interface, coreClient corev1client.CoreV1Interface, ) (*ChartInfo, error) { - repositories, err := chartproxy.NewRepoGetter(client, coreClient).List(namespace) + repositories, _, err := chartproxy.NewRepoGetter(client, coreClient).List(namespace) if err != nil { return nil, fmt.Errorf("error listing repositories: %v", err) } diff --git a/pkg/helm/chartproxy/proxy.go b/pkg/helm/chartproxy/proxy.go index 52747605522..6c65e225424 100644 --- a/pkg/helm/chartproxy/proxy.go +++ b/pkg/helm/chartproxy/proxy.go @@ -1,8 +1,8 @@ package chartproxy import ( + "encoding/json" "sort" - "strings" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" repo "helm.sh/helm/v4/pkg/repo/v1" @@ -75,12 +75,12 @@ func New(k8sConfig RestConfigProvider, kubeVersionGetter version.KubeVersionGett } func (p *proxy) IndexFile(onlyCompatible bool, namespace string) (*repo.IndexFile, error) { - helmRepos, err := p.helmRepoGetter.List(namespace) + helmRepos, configErrors, err := p.helmRepoGetter.List(namespace) if err != nil { return nil, err } annotations := make(map[string]string) - var invalidRepos []string + invalidRepos := append([]InvalidRepo{}, configErrors...) indexFile := repo.NewIndexFile() var delKeys []string = make([]string, 0, 20) for _, helmRepo := range helmRepos { @@ -88,7 +88,7 @@ func (p *proxy) IndexFile(onlyCompatible bool, namespace string) (*repo.IndexFil if !helmRepo.Disabled { idxFile, err := helmRepo.IndexFile() if err != nil { - invalidRepos = append(invalidRepos, helmRepo.Name+" ("+err.Error()+")") + invalidRepos = append(invalidRepos, InvalidRepo{Name: helmRepo.Name, Error: err.Error()}) klog.Errorf("Error retrieving index file for %v: %v", helmRepo, err) continue } @@ -125,9 +125,12 @@ func (p *proxy) IndexFile(onlyCompatible bool, namespace string) (*repo.IndexFil } } } - if invalidRepos != nil { - annotations[warning] = ErrorMessage + strings.Join(invalidRepos, ", ") - indexFile.Annotations = annotations + if len(invalidRepos) > 0 { + data, err := json.Marshal(invalidRepos) + if err == nil { + annotations[warning] = string(data) + indexFile.Annotations = annotations + } } for _, delKey := range delKeys { diff --git a/pkg/helm/chartproxy/repos.go b/pkg/helm/chartproxy/repos.go index 1ade66436bc..74ccec6e08f 100644 --- a/pkg/helm/chartproxy/repos.go +++ b/pkg/helm/chartproxy/repos.go @@ -40,9 +40,13 @@ var ( const ( configNamespace = "openshift-config" warning = "console-warning" - ErrorMessage = "The following repositories seem to be invalid or unreachable: " ) +type InvalidRepo struct { + Name string `json:"name"` + Error string `json:"error"` +} + type helmRepo struct { Name string Namespace string @@ -113,7 +117,7 @@ func (hr helmRepo) IndexFile() (*repo.IndexFile, error) { } type HelmRepoGetter interface { - List(namespace string) ([]*helmRepo, error) + List(namespace string) ([]*helmRepo, []InvalidRepo, error) } type helmRepoGetter struct { @@ -257,18 +261,21 @@ func (b helmRepoGetter) unmarshallConfig(repo unstructured.Unstructured, namespa return h, nil } -func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, error) { +func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, []InvalidRepo, error) { var helmRepos []*helmRepo + var configErrors []InvalidRepo clusterRepos, err := b.Client.Resource(helmChartRepositoryClusterGVK).List(context.TODO(), v1.ListOptions{}) if err != nil { klog.Errorf("Error listing cluster helm chart repositories: %v \nempty repository list will be used", err) - return helmRepos, nil + return helmRepos, configErrors, nil } for _, item := range clusterRepos.Items { helmConfig, err := b.unmarshallConfig(item, "", true) if err != nil { + repoName, _, _ := unstructured.NestedString(item.Object, "metadata", "name") klog.Errorf("Error unmarshalling repo %v: %v", item, err) + configErrors = append(configErrors, InvalidRepo{Name: repoName, Error: err.Error()}) continue } helmRepos = append(helmRepos, helmConfig) @@ -278,19 +285,21 @@ func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, error) { namespaceRepos, err := b.Client.Resource(helmChartRepositoryNamespaceGVK).Namespace(namespace).List(context.TODO(), v1.ListOptions{}) if err != nil { klog.Errorf("Error listing namespace helm chart repositories: %v \nempty repository list will be used", err) - return helmRepos, nil + return helmRepos, configErrors, nil } for _, item := range namespaceRepos.Items { helmConfig, err := b.unmarshallConfig(item, namespace, false) if err != nil { + repoName, _, _ := unstructured.NestedString(item.Object, "metadata", "name") klog.Errorf("Error unmarshalling repo %v: %v", item, err) + configErrors = append(configErrors, InvalidRepo{Name: repoName, Error: err.Error()}) continue } helmRepos = append(helmRepos, helmConfig) } } - return helmRepos, nil + return helmRepos, configErrors, nil } func NewRepoGetter(client dynamic.Interface, corev1Client corev1.CoreV1Interface) HelmRepoGetter { diff --git a/pkg/helm/chartproxy/repos_test.go b/pkg/helm/chartproxy/repos_test.go index 22c61baeddc..3e52549cc52 100644 --- a/pkg/helm/chartproxy/repos_test.go +++ b/pkg/helm/chartproxy/repos_test.go @@ -111,7 +111,7 @@ func TestHelmRepoGetter_List(t *testing.T) { }) } repoGetter := NewRepoGetter(client, nil) - repos, err := repoGetter.List(tt.namespace) + repos, _, err := repoGetter.List(tt.namespace) if err != nil { t.Error(err) } @@ -136,11 +136,12 @@ func TestHelmRepoGetter_List(t *testing.T) { func TestHelmRepoGetter_ListErrors(t *testing.T) { tests := []struct { - name string - helmCRS []*unstructured.Unstructured - expectedRepoName []string - apiErrors []apiError - namespace string + name string + helmCRS []*unstructured.Unstructured + expectedRepoName []string + expectedConfigErrorRepo []string + apiErrors []apiError + namespace string }{ { name: "skip repo that refer non-existent config map", @@ -179,7 +180,8 @@ func TestHelmRepoGetter_ListErrors(t *testing.T) { }, }, }, - expectedRepoName: []string{"repo2"}, + expectedRepoName: []string{"repo2"}, + expectedConfigErrorRepo: []string{"repo1"}, }, { name: "skip repo that refer config map that cannot be accessed", @@ -225,7 +227,8 @@ func TestHelmRepoGetter_ListErrors(t *testing.T) { msg: "foo", }, }, - expectedRepoName: []string{"repo2"}, + expectedRepoName: []string{"repo2"}, + expectedConfigErrorRepo: []string{"repo1"}, }, { name: "skip repo that refer secret that cannot be accessed", @@ -271,7 +274,8 @@ func TestHelmRepoGetter_ListErrors(t *testing.T) { msg: "foo", }, }, - expectedRepoName: []string{"repo2"}, + expectedRepoName: []string{"repo2"}, + expectedConfigErrorRepo: []string{"repo1"}, }, } for _, tt := range tests { @@ -284,7 +288,7 @@ func TestHelmRepoGetter_ListErrors(t *testing.T) { }) } repoGetter := NewRepoGetter(client, coreClient.CoreV1()) - repos, err := repoGetter.List(tt.namespace) + repos, configErrors, err := repoGetter.List(tt.namespace) if err != nil { t.Error(err) } @@ -296,6 +300,17 @@ func TestHelmRepoGetter_ListErrors(t *testing.T) { t.Errorf("Expected %v but got %v", repoName, repos[i].Name) } } + if len(configErrors) != len(tt.expectedConfigErrorRepo) { + t.Errorf("Expected %v config errors, but got %v", len(tt.expectedConfigErrorRepo), len(configErrors)) + } + for i, expectedName := range tt.expectedConfigErrorRepo { + if configErrors[i].Name != expectedName { + t.Errorf("Expected config error repo %v but got %v", expectedName, configErrors[i].Name) + } + if configErrors[i].Error == "" { + t.Errorf("Expected non-empty error message for repo %v", expectedName) + } + } }) } } @@ -499,7 +514,7 @@ func TestHelmRepoGetter_SkipDisabled(t *testing.T) { client := fake.K8sDynamicClientFromCRs(tt.helmCRS...) coreClient := k8sfake.NewSimpleClientset() repoGetter := NewRepoGetter(client, coreClient.CoreV1()) - repos, err := repoGetter.List(tt.namespace) + repos, _, err := repoGetter.List(tt.namespace) if err != nil { t.Error(err) } From 66e89b450ec23febed476b9c2d7aa39d19838f68 Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Tue, 21 Jul 2026 17:01:50 +0200 Subject: [PATCH 3/8] HELM-586: Address review feedback for chartproxy error handling Use t.Fatalf for config error count mismatch to prevent index-out-of-bounds panic in subsequent loop. Log resp.Body.Close() errors instead of silently discarding them. Add fallback name for repos where metadata.name extraction fails. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Szuc --- pkg/helm/chartproxy/repos.go | 12 +++++++++++- pkg/helm/chartproxy/repos_test.go | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/helm/chartproxy/repos.go b/pkg/helm/chartproxy/repos.go index 74ccec6e08f..dc77a2a6c8d 100644 --- a/pkg/helm/chartproxy/repos.go +++ b/pkg/helm/chartproxy/repos.go @@ -87,7 +87,11 @@ func (hr helmRepo) IndexFile() (*repo.IndexFile, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + klog.Errorf("Error closing response body for %v: %v", indexURL, err) + } + }() if resp.StatusCode == 401 || resp.StatusCode == 403 { return nil, fmt.Errorf("authentication failed (HTTP %d)", resp.StatusCode) } @@ -274,6 +278,9 @@ func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, []InvalidRepo, err helmConfig, err := b.unmarshallConfig(item, "", true) if err != nil { repoName, _, _ := unstructured.NestedString(item.Object, "metadata", "name") + if repoName == "" { + repoName = "" + } klog.Errorf("Error unmarshalling repo %v: %v", item, err) configErrors = append(configErrors, InvalidRepo{Name: repoName, Error: err.Error()}) continue @@ -291,6 +298,9 @@ func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, []InvalidRepo, err helmConfig, err := b.unmarshallConfig(item, namespace, false) if err != nil { repoName, _, _ := unstructured.NestedString(item.Object, "metadata", "name") + if repoName == "" { + repoName = "" + } klog.Errorf("Error unmarshalling repo %v: %v", item, err) configErrors = append(configErrors, InvalidRepo{Name: repoName, Error: err.Error()}) continue diff --git a/pkg/helm/chartproxy/repos_test.go b/pkg/helm/chartproxy/repos_test.go index 3e52549cc52..baf21878609 100644 --- a/pkg/helm/chartproxy/repos_test.go +++ b/pkg/helm/chartproxy/repos_test.go @@ -301,7 +301,7 @@ func TestHelmRepoGetter_ListErrors(t *testing.T) { } } if len(configErrors) != len(tt.expectedConfigErrorRepo) { - t.Errorf("Expected %v config errors, but got %v", len(tt.expectedConfigErrorRepo), len(configErrors)) + t.Fatalf("Expected %v config errors, but got %v", len(tt.expectedConfigErrorRepo), len(configErrors)) } for i, expectedName := range tt.expectedConfigErrorRepo { if configErrors[i].Name != expectedName { From 6dc47dfbff1fb5a5e8817e01a939e2f7993103ba Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Tue, 21 Jul 2026 17:12:19 +0200 Subject: [PATCH 4/8] HELM-586: Redact credentials from URL in error log Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Szuc --- pkg/helm/chartproxy/repos.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/helm/chartproxy/repos.go b/pkg/helm/chartproxy/repos.go index dc77a2a6c8d..9607b1cf067 100644 --- a/pkg/helm/chartproxy/repos.go +++ b/pkg/helm/chartproxy/repos.go @@ -87,11 +87,7 @@ func (hr helmRepo) IndexFile() (*repo.IndexFile, error) { if err != nil { return nil, err } - defer func() { - if err := resp.Body.Close(); err != nil { - klog.Errorf("Error closing response body for %v: %v", indexURL, err) - } - }() + defer resp.Body.Close() if resp.StatusCode == 401 || resp.StatusCode == 403 { return nil, fmt.Errorf("authentication failed (HTTP %d)", resp.StatusCode) } From 6b7261ff926f568bea2bb90a52faaa4a59e84f2e Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Wed, 22 Jul 2026 00:04:20 +0200 Subject: [PATCH 5/8] HELM-586: Address review feedback for chartproxy error handling Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Szuc --- .../src/catalog/providers/useHelmCharts.tsx | 4 +- pkg/helm/chartproxy/proxy.go | 7 ++- pkg/helm/chartproxy/repos.go | 51 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx index 86a9538bda6..f56abda7892 100644 --- a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx +++ b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx @@ -66,9 +66,7 @@ const useHelmCharts: ExtensionHook = ({ shownWarningRef.current = warningData; try { const repos: { name: string; error: string }[] = JSON.parse(warningData); - const repoList = repos - .map((r) => t('{{name}}: {{error}}', { name: r.name, error: r.error })) - .join(', '); + const repoList = repos.map((r) => `${r.name}: ${r.error}`).join(', '); toast.addToast({ variant: AlertVariant.danger, title: t('Helm Chart repository error'), diff --git a/pkg/helm/chartproxy/proxy.go b/pkg/helm/chartproxy/proxy.go index 6c65e225424..936e9fa3d2b 100644 --- a/pkg/helm/chartproxy/proxy.go +++ b/pkg/helm/chartproxy/proxy.go @@ -2,6 +2,7 @@ package chartproxy import ( "encoding/json" + "slices" "sort" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" @@ -80,7 +81,7 @@ func (p *proxy) IndexFile(onlyCompatible bool, namespace string) (*repo.IndexFil return nil, err } annotations := make(map[string]string) - invalidRepos := append([]InvalidRepo{}, configErrors...) + invalidRepos := slices.Clone(configErrors) indexFile := repo.NewIndexFile() var delKeys []string = make([]string, 0, 20) for _, helmRepo := range helmRepos { @@ -127,7 +128,9 @@ func (p *proxy) IndexFile(onlyCompatible bool, namespace string) (*repo.IndexFil } if len(invalidRepos) > 0 { data, err := json.Marshal(invalidRepos) - if err == nil { + if err != nil { + klog.Errorf("Error marshalling invalid repos annotation: %v", err) + } else { annotations[warning] = string(data) indexFile.Annotations = annotations } diff --git a/pkg/helm/chartproxy/repos.go b/pkg/helm/chartproxy/repos.go index 9607b1cf067..8f5165c881d 100644 --- a/pkg/helm/chartproxy/repos.go +++ b/pkg/helm/chartproxy/repos.go @@ -261,28 +261,37 @@ func (b helmRepoGetter) unmarshallConfig(repo unstructured.Unstructured, namespa return h, nil } -func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, []InvalidRepo, error) { - var helmRepos []*helmRepo +func (b *helmRepoGetter) processRepoItems(items []unstructured.Unstructured, namespace string, isClusterScoped bool) ([]*helmRepo, []InvalidRepo) { + var repos []*helmRepo var configErrors []InvalidRepo - - clusterRepos, err := b.Client.Resource(helmChartRepositoryClusterGVK).List(context.TODO(), v1.ListOptions{}) - if err != nil { - klog.Errorf("Error listing cluster helm chart repositories: %v \nempty repository list will be used", err) - return helmRepos, configErrors, nil - } - for _, item := range clusterRepos.Items { - helmConfig, err := b.unmarshallConfig(item, "", true) + for _, item := range items { + helmConfig, err := b.unmarshallConfig(item, namespace, isClusterScoped) if err != nil { - repoName, _, _ := unstructured.NestedString(item.Object, "metadata", "name") - if repoName == "" { + repoName, found, nameErr := unstructured.NestedString(item.Object, "metadata", "name") + if nameErr != nil || !found { repoName = "" } klog.Errorf("Error unmarshalling repo %v: %v", item, err) configErrors = append(configErrors, InvalidRepo{Name: repoName, Error: err.Error()}) continue } - helmRepos = append(helmRepos, helmConfig) + repos = append(repos, helmConfig) } + return repos, configErrors +} + +func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, []InvalidRepo, error) { + var helmRepos []*helmRepo + var configErrors []InvalidRepo + + clusterRepos, err := b.Client.Resource(helmChartRepositoryClusterGVK).List(context.TODO(), v1.ListOptions{}) + if err != nil { + klog.Errorf("Error listing cluster helm chart repositories: %v \nempty repository list will be used", err) + return helmRepos, configErrors, nil + } + repos, errs := b.processRepoItems(clusterRepos.Items, "", true) + helmRepos = append(helmRepos, repos...) + configErrors = append(configErrors, errs...) if namespace != "" { namespaceRepos, err := b.Client.Resource(helmChartRepositoryNamespaceGVK).Namespace(namespace).List(context.TODO(), v1.ListOptions{}) @@ -290,19 +299,9 @@ func (b *helmRepoGetter) List(namespace string) ([]*helmRepo, []InvalidRepo, err klog.Errorf("Error listing namespace helm chart repositories: %v \nempty repository list will be used", err) return helmRepos, configErrors, nil } - for _, item := range namespaceRepos.Items { - helmConfig, err := b.unmarshallConfig(item, namespace, false) - if err != nil { - repoName, _, _ := unstructured.NestedString(item.Object, "metadata", "name") - if repoName == "" { - repoName = "" - } - klog.Errorf("Error unmarshalling repo %v: %v", item, err) - configErrors = append(configErrors, InvalidRepo{Name: repoName, Error: err.Error()}) - continue - } - helmRepos = append(helmRepos, helmConfig) - } + repos, errs := b.processRepoItems(namespaceRepos.Items, namespace, false) + helmRepos = append(helmRepos, repos...) + configErrors = append(configErrors, errs...) } return helmRepos, configErrors, nil From 9f7e777306e6dc240c3fffd1b1c12b29c22963cb Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Wed, 22 Jul 2026 00:19:49 +0200 Subject: [PATCH 6/8] HELM-586: Deduplicate repo warning toast per session using sessionStorage Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Szuc --- .../src/catalog/providers/useHelmCharts.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx index f56abda7892..2caabc86afc 100644 --- a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx +++ b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx @@ -62,8 +62,18 @@ const useHelmCharts: ExtensionHook = ({ }; setHelmCharts(json.entries); const warningData = json.annotations?.['console-warning']; - if (warningData && warningData !== shownWarningRef.current) { + const sessionKey = 'helm-repo-warning-shown'; + if ( + warningData && + warningData !== shownWarningRef.current && + warningData !== sessionStorage.getItem(sessionKey) + ) { shownWarningRef.current = warningData; + try { + sessionStorage.setItem(sessionKey, warningData); + } catch { + // sessionStorage may be unavailable + } try { const repos: { name: string; error: string }[] = JSON.parse(warningData); const repoList = repos.map((r) => `${r.name}: ${r.error}`).join(', '); From e61d1ae983b7284d3b638130eabe9a8c360e617a Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Thu, 23 Jul 2026 23:57:28 +0200 Subject: [PATCH 7/8] HELM-586: Disable auto-dismiss on repo warning toast Error toasts for unreachable Helm Chart repositories should persist until manually dismissed so users do not miss them. Signed-off-by: Martin Szuc --- .../helm-plugin/src/catalog/providers/useHelmCharts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx index 2caabc86afc..80c5aac924a 100644 --- a/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx +++ b/frontend/packages/helm-plugin/src/catalog/providers/useHelmCharts.tsx @@ -84,7 +84,7 @@ const useHelmCharts: ExtensionHook = ({ repoList, }), dismissible: true, - timeout: true, + timeout: false, }); } catch { // ignore malformed annotation From 74f713502d4558477ae49820c2e87ec879c0b28e Mon Sep 17 00:00:00 2001 From: Martin Szuc Date: Fri, 24 Jul 2026 00:28:57 +0200 Subject: [PATCH 8/8] HELM-586: Add i18n keys for Helm Chart repository error toast Signed-off-by: Martin Szuc --- frontend/packages/helm-plugin/locales/en/helm-plugin.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/packages/helm-plugin/locales/en/helm-plugin.json b/frontend/packages/helm-plugin/locales/en/helm-plugin.json index 7c52d917308..fbebb1f70f2 100644 --- a/frontend/packages/helm-plugin/locales/en/helm-plugin.json +++ b/frontend/packages/helm-plugin/locales/en/helm-plugin.json @@ -62,6 +62,7 @@ "Helm Chart": "Helm Chart", "Helm Chart cannot be installed": "Helm Chart cannot be installed", "Helm Chart repositories": "Helm Chart repositories", + "Helm Chart repository error": "Helm Chart repository error", "Helm Chart repository URL.": "Helm Chart repository URL.", "Helm chart URL": "Helm chart URL", "Helm Charts": "Helm Charts", @@ -138,6 +139,7 @@ "Source": "Source", "Status": "Status", "Status Box": "Status Box", + "The following repositories are unreachable: {{repoList}}": "The following repositories are unreachable: {{repoList}}", "The Helm Chart is currently unavailable. {{chartError}}": "The Helm Chart is currently unavailable. {{chartError}}", "The Helm Release can be created by completing the form. Default values may be provided by the Helm chart authors.": "The Helm Release can be created by completing the form. Default values may be provided by the Helm chart authors.", "The Helm Release can be created by manually entering YAML or JSON definitions.": "The Helm Release can be created by manually entering YAML or JSON definitions.",