From 6eac5258a11552aadc25a2ca1d3518d9a9ba821b Mon Sep 17 00:00:00 2001 From: sowmya-sl Date: Mon, 20 Jul 2026 15:31:39 +0530 Subject: [PATCH 1/7] HELM-763: Add basic auth secret support for Helm upgrade and chart fetching - Pass basicAuthSecretName through UpgradeReleaseAsync to support auth credentials during chart upgrades - Use __none__ sentinel to allow users to explicitly clear a secret - Preserve installation annotation on upgrade to maintain URL-install tracking across revisions - Return errors instead of logging on auth credential failures so the frontend can surface them - Add 401/unauthorized detection in GetChartFromURL, InstallChartFromURL, and UpgradeReleaseAsync with actionable error messages - Update handler signature, mock, and tests for the new parameter --- pkg/helm/actions/get_chart.go | 4 +++ pkg/helm/actions/install_chart.go | 3 +++ pkg/helm/actions/upgrade_release.go | 32 ++++++++++++++++++------ pkg/helm/actions/upgrade_release_test.go | 8 +++--- pkg/helm/handlers/handler_test.go | 4 +-- pkg/helm/handlers/handlers.go | 4 +-- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/pkg/helm/actions/get_chart.go b/pkg/helm/actions/get_chart.go index d82dab25411..6c607542a34 100644 --- a/pkg/helm/actions/get_chart.go +++ b/pkg/helm/actions/get_chart.go @@ -3,6 +3,7 @@ package actions import ( "fmt" "os" + "strings" "github.com/openshift/api/helm/v1beta1" "helm.sh/helm/v4/pkg/action" @@ -83,6 +84,9 @@ func GetChartFromURL(url string, conf *action.Configuration, namespace string, c } chartLocation, err := cmd.ChartPathOptions.LocateChart(url, settings) if err != nil { + if basicAuthSecretName == "" && (strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "unauthorized")) { + return nil, fmt.Errorf("error getting chart from URL: %w; registry requires authentication - select a Secret with \"username\" and \"password\" keys for basic authentication", err) + } return nil, fmt.Errorf("error getting chart from URL: %v", err) } return loader.Load(chartLocation) diff --git a/pkg/helm/actions/install_chart.go b/pkg/helm/actions/install_chart.go index 9cb08a05339..439adce3051 100644 --- a/pkg/helm/actions/install_chart.go +++ b/pkg/helm/actions/install_chart.go @@ -358,6 +358,9 @@ func InstallChartFromURL(ns, name, url string, vals map[string]interface{}, conf cp, err := cmd.ChartPathOptions.LocateChart(url, settings) if err != nil { + if basicAuthSecretName == "" && (strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "unauthorized")) { + return nil, fmt.Errorf("error locating chart: %w; registry requires authentication - select a Secret with \"username\" and \"password\" keys for basic authentication", err) + } return nil, fmt.Errorf("error locating chart: %v", err) } ch, err := loader.Load(cp) diff --git a/pkg/helm/actions/upgrade_release.go b/pkg/helm/actions/upgrade_release.go index 25d007d38aa..f93b4dced7b 100644 --- a/pkg/helm/actions/upgrade_release.go +++ b/pkg/helm/actions/upgrade_release.go @@ -19,7 +19,6 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/klog/v2" ) func UpgradeRelease( @@ -107,12 +106,15 @@ func UpgradeRelease( return nil, err } - // Ensure chart URL is properly set in the upgrade chart + // Ensure chart URL and installation method are properly set in the upgrade chart if chartUrl != "" { if ch.Metadata.Annotations == nil { ch.Metadata.Annotations = make(map[string]string) } ch.Metadata.Annotations["chart_url"] = chartUrl + if inst, ok := rel.Chart.Metadata.Annotations["installation"]; ok { + ch.Metadata.Annotations["installation"] = inst + } } result, err := client.Run(releaseName, ch, vals) @@ -149,6 +151,7 @@ func UpgradeReleaseAsync( coreClient corev1client.CoreV1Interface, fileCleanUp bool, indexEntry string, + basicAuthSecretName string, ) (*kv1.Secret, error) { client := action.NewUpgrade(conf) client.ServerSideApply = "false" @@ -167,14 +170,21 @@ func UpgradeReleaseAsync( return nil, err } - auth_secret := "" + auth_secret := basicAuthSecretName + // "__none__" is a sentinel from the frontend meaning the user explicitly cleared the secret. + explicitlyClearedSecret := auth_secret == "__none__" + if explicitlyClearedSecret { + auth_secret = "" + } // Before proceeding, check if chart URL is present as an annotation - if rel.Chart.Metadata.Annotations != nil { + if rel.Chart.Metadata != nil && rel.Chart.Metadata.Annotations != nil { if chart_url, ok := rel.Chart.Metadata.Annotations["chart_url"]; chartUrl == "" && ok { chartUrl = chart_url } - if authSecret, ok := rel.Chart.Metadata.Annotations[helmAuthSecretAnnotation]; ok { - auth_secret = authSecret + if auth_secret == "" && !explicitlyClearedSecret { + if authSecret, ok := rel.Chart.Metadata.Annotations[helmAuthSecretAnnotation]; ok { + auth_secret = authSecret + } } } @@ -215,10 +225,10 @@ func UpgradeReleaseAsync( if auth_secret != "" { userCredentials, err := GetUserCredentials(coreClient, releaseNamespace, auth_secret) if err != nil { - klog.Errorf("Failed to get user credentials Secret %s for release upgrade %s/%s: %v", auth_secret, releaseNamespace, releaseName, err) + return nil, fmt.Errorf("failed to get user credentials Secret %s for release upgrade %s/%s: %v", auth_secret, releaseNamespace, releaseName, err) } else { if err := applyBasicAuthFromUserCredentials(&client.ChartPathOptions, client, userCredentials); err != nil { - klog.Errorf("Failed to apply auth from Secret %s for release upgrade %s/%s: %v", auth_secret, releaseNamespace, releaseName, err) + return nil, fmt.Errorf("failed to apply auth from Secret %s for release upgrade %s/%s: %v", auth_secret, releaseNamespace, releaseName, err) } } } @@ -226,6 +236,9 @@ func UpgradeReleaseAsync( client.ChartPathOptions.Version = chartInfo.Version cp, err = client.ChartPathOptions.LocateChart(chartLocation, settings) if err != nil { + if auth_secret == "" && (strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "unauthorized")) { + return nil, fmt.Errorf("failed to upgrade helm release: %w; registry requires authentication - select a Secret with \"username\" and \"password\" keys for basic authentication", err) + } return nil, err } ch, err = loader.Load(cp) @@ -247,6 +260,9 @@ func UpgradeReleaseAsync( } if chartUrl != "" { ch.Metadata.Annotations["chart_url"] = chartUrl + if inst, ok := rel.Chart.Metadata.Annotations["installation"]; ok { + ch.Metadata.Annotations["installation"] = inst + } addAuthSecretAnnotation(ch, auth_secret) } go func() { diff --git a/pkg/helm/actions/upgrade_release_test.go b/pkg/helm/actions/upgrade_release_test.go index 6f123437f6c..ab969ccfc53 100644 --- a/pkg/helm/actions/upgrade_release_test.go +++ b/pkg/helm/actions/upgrade_release_test.go @@ -502,7 +502,7 @@ func TestUpgradeReleaseWithoutDependenciesAsync(t *testing.T) { var rel *v1.Secret var err error go func() { - rel, err = UpgradeReleaseAsync(tt.namespace, tt.releaseName, tt.chartPath, nil, actionConfig, client, coreClient, false, tt.indexEntry) + rel, err = UpgradeReleaseAsync(tt.namespace, tt.releaseName, tt.chartPath, nil, actionConfig, client, coreClient, false, tt.indexEntry, "") if tt.requireErr { fmt.Println("Error", err) require.Error(t, err) @@ -597,7 +597,7 @@ func TestUpgradeReleaseWithDependenciesAsync(t *testing.T) { store.Create(&r) go func() { - rel, err = UpgradeReleaseAsync(tt.releaseNamespace, tt.releaseName, tt.chartPath, tt.values, actionConfig, client, coreClient, true, tt.indexEntry) + rel, err = UpgradeReleaseAsync(tt.releaseNamespace, tt.releaseName, tt.chartPath, tt.values, actionConfig, client, coreClient, true, tt.indexEntry, "") require.NoError(t, err) require.Equal(t, fmt.Sprintf("sh.helm.release.v1.%v.v2", tt.releaseName), rel.ObjectMeta.Name) }() @@ -688,7 +688,7 @@ func TestUpgradeReleaseWithCustomValuesAsync(t *testing.T) { var rel *v1.Secret var err error go func() { - rel, err = UpgradeReleaseAsync(tt.releaseNamespace, tt.releaseName, tt.chartPath, tt.values, actionConfig, client, coreClient, true, tt.indexEntry) + rel, err = UpgradeReleaseAsync(tt.releaseNamespace, tt.releaseName, tt.chartPath, tt.values, actionConfig, client, coreClient, true, tt.indexEntry, "") require.NoError(t, err) require.Equal(t, fmt.Sprintf("sh.helm.release.v1.%v.v2", tt.releaseName), rel.ObjectMeta.Name) }() @@ -775,7 +775,7 @@ func TestUpgradeAfterURLInstallWithSecrets(t *testing.T) { // Upgrade — chartUrl is recovered from the annotation, auth credentials are applied secretsDriver := driver.NewSecrets(coreClient.Secrets(tt.releaseNamespace)) go func() { - upgradeResult, upgradeErr := UpgradeReleaseAsync(tt.releaseNamespace, tt.releaseName, "", nil, actionConfig, dynamicClient, coreClient, true, "") + upgradeResult, upgradeErr := UpgradeReleaseAsync(tt.releaseNamespace, tt.releaseName, "", nil, actionConfig, dynamicClient, coreClient, true, "", "") require.NoError(t, upgradeErr) require.Equal(t, fmt.Sprintf("sh.helm.release.v1.%v.v2", tt.releaseName), upgradeResult.ObjectMeta.Name) }() diff --git a/pkg/helm/handlers/handler_test.go b/pkg/helm/handlers/handler_test.go index 0e8b2409c5c..f4e17fdc6e0 100644 --- a/pkg/helm/handlers/handler_test.go +++ b/pkg/helm/handlers/handler_test.go @@ -137,8 +137,8 @@ func fakeUpgradeRelease(name, ns string, t *testing.T, fakeRelease *releasev1.Re } } -func fakeUpgradeReleaseAsync(name, ns string, t *testing.T, fakeSecret *kv1.Secret, err error) func(ns, name, url string, vals map[string]interface{}, conf *action.Configuration, client dynamic.Interface, coreClient corev1client.CoreV1Interface, fileCleanUp bool, indexEntry string) (*kv1.Secret, error) { - return func(namespace, n, url string, vals map[string]interface{}, conf *action.Configuration, client dynamic.Interface, coreClient corev1client.CoreV1Interface, fileCleanUp bool, indexEntry string) (*kv1.Secret, error) { +func fakeUpgradeReleaseAsync(name, ns string, t *testing.T, fakeSecret *kv1.Secret, err error) func(ns, name, url string, vals map[string]interface{}, conf *action.Configuration, client dynamic.Interface, coreClient corev1client.CoreV1Interface, fileCleanUp bool, indexEntry string, basicAuthSecretName string) (*kv1.Secret, error) { + return func(namespace, n, url string, vals map[string]interface{}, conf *action.Configuration, client dynamic.Interface, coreClient corev1client.CoreV1Interface, fileCleanUp bool, indexEntry string, basicAuthSecretName string) (*kv1.Secret, error) { if namespace != ns { t.Errorf("Namespace mismatch expected %s received %s", ns, namespace) } diff --git a/pkg/helm/handlers/handlers.go b/pkg/helm/handlers/handlers.go index 4cfefa057d1..2d288acd165 100644 --- a/pkg/helm/handlers/handlers.go +++ b/pkg/helm/handlers/handlers.go @@ -67,7 +67,7 @@ type helmHandlers struct { installChart func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string) (*releasev1.Release, error) installChartFromURL func(string, string, string, map[string]interface{}, *action.Configuration, corev1client.CoreV1Interface, string, string) (*kv1.Secret, error) listReleases func(*action.Configuration, bool) ([]*releasev1.Release, error) - upgradeReleaseAsync func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string) (*kv1.Secret, error) + upgradeReleaseAsync func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string, string) (*kv1.Secret, error) upgradeRelease func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string) (*releasev1.Release, error) uninstallRelease func(string, *action.Configuration) (*releasecommon.UninstallReleaseResponse, error) uninstallReleaseAsync func(string, string, string, *action.Configuration, corev1client.CoreV1Interface) error @@ -309,7 +309,7 @@ func (h *helmHandlers) HandleUpgradeReleaseAsync(user *auth.User, w http.Respons serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: err.Error()}) return } - resp, err := h.upgradeReleaseAsync(req.Namespace, req.Name, req.ChartUrl, req.Values, conf, handlerClients.DynamicClient, handlerClients.CoreClient, false, req.IndexEntry) + resp, err := h.upgradeReleaseAsync(req.Namespace, req.Name, req.ChartUrl, req.Values, conf, handlerClients.DynamicClient, handlerClients.CoreClient, false, req.IndexEntry, req.BasicAuthSecretName) if err != nil { if err.Error() == actions.ErrReleaseRevisionNotFound.Error() { serverutils.SendResponse(w, http.StatusNotFound, serverutils.ApiError{Err: fmt.Sprintf("Failed to rollback helm releases: %v", err)}) From 1c9f554a5ae56374ab6666798b7545c3fa51d61a Mon Sep 17 00:00:00 2001 From: sowmya-sl Date: Mon, 20 Jul 2026 16:59:04 +0530 Subject: [PATCH 2/7] HELM-763: Add basic auth secret support to Helm install and upgrade forms - Add HelmCreateBasicAuthSecretModal for creating kubernetes.io/basic-auth secrets with username/password from within the Helm forms - Add auth secret dropdown with 'None' and 'Create Secret' options to HelmInstallUpgradeForm, shown when upgrading a URL-installed chart - Detect URL-installed charts via 'installation' annotation and read initial auth secret from 'helm.openshift.io/auth-secret' annotation - Pass basicAuthSecretName through to the upgrade API payload - Use __none__ sentinel to allow explicitly clearing a previously set secret - Show warning when a previously referenced secret no longer exists - Add auth secret dropdown to HelmURLChartForm for URL-based chart installs --- .../helm-plugin/locales/en/helm-plugin.json | 69 +++++++ .../HelmInstallUpgradeForm.tsx | 99 ++++++++- .../HelmInstallUpgradePage.tsx | 10 + .../HelmCreateBasicAuthSecretModal.tsx | 188 ++++++++++++++++++ .../forms/url-chart/HelmURLChartForm.tsx | 40 +++- 5 files changed, 403 insertions(+), 3 deletions(-) create mode 100644 frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx diff --git a/frontend/packages/helm-plugin/locales/en/helm-plugin.json b/frontend/packages/helm-plugin/locales/en/helm-plugin.json index 7c52d917308..24437966a4e 100644 --- a/frontend/packages/helm-plugin/locales/en/helm-plugin.json +++ b/frontend/packages/helm-plugin/locales/en/helm-plugin.json @@ -40,6 +40,75 @@ "Configure Helm release": "Configure Helm release", "Create": "Create", "Create Helm Chart Repository": "Create Helm Chart Repository", + "Change chart version?": "Change chart version?", + "Are you sure you want to change the chart version from <2>{{currentVersion}} to <4>{{newVersion}}? ": "Are you sure you want to change the chart version from <2>{{currentVersion}} to <4>{{newVersion}}? ", + "Values from your current release are merged with the new chart's defaults. Review the YAML or form before upgrading.": "Values from your current release are merged with the new chart's defaults. Review the YAML or form before upgrading.", + "Proceed": "Proceed", + "Select the version to upgrade to.": "Select the version to upgrade to.", + "No versions available": "No versions available", + "Helm Release": "Helm Release", + "For more information on the chart, refer to this <2>README": "For more information on the chart, refer to this <2>README", + "Helm Chart cannot be installed": "Helm Chart cannot be installed", + "The Helm Chart is currently unavailable. {{chartError}}": "The Helm Chart is currently unavailable. {{chartError}}", + "Release name": "Release name", + "A unique name for the Helm Release.": "A unique name for the Helm Release.", + "Secret for basic authentication": "Secret for basic authentication", + "None": "None", + "Select a secret": "Select a secret", + "Create Secret": "Create Secret", + "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.": "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.", + "Secret \"{{secretName}}\" was not found in this namespace. Select an existing secret or create a new one.": "Secret \"{{secretName}}\" was not found in this namespace. Select an existing secret or create a new one.", + "Helm release is not configurable since the Helm Chart doesn't define any values.": "Helm release is not configurable since the Helm Chart doesn't define any values.", + "Errors in the form data.": "Errors in the form data.", + "Invalid Form Schema - {{errorText}}": "Invalid Form Schema - {{errorText}}", + "Invalid YAML - {{errorText}}": "Invalid YAML - {{errorText}}", + "Select the version to rollback <1>{{releaseName}} to, from the table below:": "Select the version to rollback <1>{{releaseName}} to, from the table below:", + "Select": "Select", + "Must be a valid OCI URL or a valid HTTP/HTTPS tar file; for example - oci://registry.example.com/chart, https://example.com/chart-1.0.0.tgz.": "Must be a valid OCI URL or a valid HTTP/HTTPS tar file; for example - oci://registry.example.com/chart, https://example.com/chart-1.0.0.tgz.", + "Failed to create Secret.": "Failed to create Secret.", + "Create authentication Secret": "Create authentication Secret", + "Secret name": "Secret name", + "Unique name of the Secret.": "Unique name of the Secret.", + "Secret username": "Secret username", + "Username for OCI/HTTP(S) authentication.": "Username for OCI/HTTP(S) authentication.", + "Secret password or token": "Secret password or token", + "Password or token for OCI/HTTP(S) authentication.": "Password or token for OCI/HTTP(S) authentication.", + "Invalid chart URL format.": "Invalid chart URL format.", + "Install Helm chart from URL": "Install Helm chart from URL", + "To install a Helm chart, enter the chart URL - Open Container Initiative (OCI) URL or HTTP/HTTPS tar file and version.": "To install a Helm chart, enter the chart URL - Open Container Initiative (OCI) URL or HTTP/HTTPS tar file and version.", + "Chart URL": "Chart URL", + "The OCI URL or HTTP/HTTPS tar file for the Helm chart; for example - oci://registry.example.com/charts/mychart or https://example.com/chart-1.0.0.tgz.": "The OCI URL or HTTP/HTTPS tar file for the Helm chart; for example - oci://registry.example.com/charts/mychart or https://example.com/chart-1.0.0.tgz.", + "Unique name for Helm release.": "Unique name for Helm release.", + "The version of chart to install.": "The version of chart to install.", + "Secret for Basic authentication": "Secret for Basic authentication", + "Next": "Next", + "Install Helm chart from Helm registry.": "Install Helm chart from Helm registry.", + "Helm release": "Helm release", + "Complete the form to create a Helm release. The Helm chart authors might have provided some default values.": "Complete the form to create a Helm release. The Helm chart authors might have provided some default values.", + "Configure Helm release": "Configure Helm release", + "Version": "Version", + "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication": "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication", + "Install": "Install", + "Back": "Back", + "Namespace": "Namespace", + "Unable to load Helm Releases": "Unable to load Helm Releases", + "Filter by status": "Filter by status", + "No Helm Releases found": "No Helm Releases found", + "Browse the catalog to discover available Helm Charts": "Browse the catalog to discover available Helm Charts", + "Helm chart URL": "Helm chart URL", + "Repositories": "Repositories", + "Select a Project to view its details<1>.": "Select a Project to view its details<1>.", + "All Namespaces": "All Namespaces", + "True": "True", + "False": "False", + "Display Name": "Display Name", + "Disabled": "Disabled", + "Repo URL": "Repo URL", + "Helm Repositories": "Helm Repositories", + "Helm release is {{status}}": "Helm release is {{status}}", + "Unable to find resource for {{helmLabel}}": "Unable to find resource for {{helmLabel}}", + " / App Version {{appVersion}}": " / App Version {{appVersion}}", + " (Provided by {{chartRepoName}})": " (Provided by {{chartRepoName}})", "Create Helm Release": "Create Helm Release", "Create ProjectHelmChartRepository": "Create ProjectHelmChartRepository", "Created": "Created", diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx index 50d40c31ee4..185e40becc7 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx @@ -1,7 +1,8 @@ import type { ReactNode, FC } from 'react'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { TextInputTypes, Grid, GridItem, Button, Alert } from '@patternfly/react-core'; import type { FormikProps } from 'formik'; +import * as fuzzy from 'fuzzysearch'; import type { JSONSchema7 } from 'json-schema'; import * as _ from 'lodash'; import { Trans, useTranslation } from 'react-i18next'; @@ -14,11 +15,14 @@ import { FormHeader } from '@console/shared/src/components/form-utils/FormHeader import { CodeEditorField } from '@console/shared/src/components/formik-fields/CodeEditorField'; import { DynamicFormField } from '@console/shared/src/components/formik-fields/DynamicFormField'; import { InputField } from '@console/shared/src/components/formik-fields/InputField'; +import { ResourceDropdownField } from '@console/shared/src/components/formik-fields/ResourceDropdownField'; import { SyncedEditorField } from '@console/shared/src/components/formik-fields/SyncedEditorField'; import type { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle'; import type { HelmChart, HelmActionConfigType } from '../../../types/helm-types'; import { HelmActionType } from '../../../types/helm-types'; import { helmActionString } from '../../../utils/helm-utils'; +import { useHelmCreateBasicAuthSecretModal } from '../url-chart/HelmCreateBasicAuthSecretModal'; +import { useSecretResources } from '../url-chart/useSecretResources'; import HelmChartVersionDropdown from './HelmChartVersionDropdown'; import { useHelmReadmeModalLauncher } from './HelmReadmeModal'; @@ -35,6 +39,8 @@ export type HelmInstallUpgradeFormData = { formData: any; formSchema: JSONSchema7; editorType: EditorType; + basicAuthSecretName?: string; + isURLInstall?: boolean; }; interface HelmInstallUpgradeFormProps { @@ -68,11 +74,61 @@ const HelmInstallUpgradeForm: FC< chartIndexEntry, annotatedName, providerName, + setFieldValue, }) => { const { t } = useTranslation('helm-plugin'); + const launchHelmCreateBasicAuthSecretModal = useHelmCreateBasicAuthSecretModal(); + const [isCreateSecretModalOpen, setIsCreateSecretModalOpen] = useState(false); + + const CREATE_SECRET_KEY = 'create-secret'; + const NONE_SECRET_KEY = '__none__'; + + const handleSecretSave = (name: string) => { + setFieldValue('basicAuthSecretName', name); + }; + + const handleSecretChange = (key: string) => { + if (key === NONE_SECRET_KEY) { + window.setTimeout(() => setFieldValue('basicAuthSecretName', NONE_SECRET_KEY), 0); + return; + } + if (key === CREATE_SECRET_KEY && !isCreateSecretModalOpen) { + // ResourceDropdownField writes the selected key to form state after this callback. + // Defer restoring the previous secret so "create-secret" is not persisted. + window.setTimeout( + () => setFieldValue('basicAuthSecretName', values.basicAuthSecretName || ''), + 0, + ); + setIsCreateSecretModalOpen(true); + launchHelmCreateBasicAuthSecretModal({ + namespace, + save: (name) => { + handleSecretSave(name); + setIsCreateSecretModalOpen(false); + }, + onClose: () => setIsCreateSecretModalOpen(false), + }); + } + }; const { chartName, chartVersion, chartReadme, formData, formSchema, editorType } = values; const { type: helmAction, title, subTitle } = helmActionConfig; const helmReadmeModalLauncher = useHelmReadmeModalLauncher({ readme: chartReadme }); + const showAuthSecret = values.isURLInstall; + const secretResources = useSecretResources(namespace); + const autocompleteFilter = (strText: string, item: any): boolean => + fuzzy(strText, item?.props?.name); + const secretMissing = useMemo(() => { + if ( + !showAuthSecret || + !values.basicAuthSecretName || + values.basicAuthSecretName === NONE_SECRET_KEY || + !secretResources[0]?.loaded + ) { + return false; + } + const secrets = secretResources[0]?.data ?? []; + return !secrets.some((s) => s?.metadata?.name === values.basicAuthSecretName); + }, [showAuthSecret, secretResources, values.basicAuthSecretName]); const isSubmitDisabled = (helmAction === HelmActionType.Upgrade && !dirty) || isSubmitting || @@ -162,6 +218,47 @@ const HelmInstallUpgradeForm: FC< providerName={providerName} /> + {showAuthSecret && ( + + + {secretMissing && ( + + )} + + )} {!chartError && diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx index 378e17679a0..c850ba51daa 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx @@ -65,6 +65,7 @@ const HelmInstallUpgradePage: FC = () => { const [initialYamlData, setInitialYamlData] = useState(''); const [initialFormData, setInitialFormData] = useState(); const [initialFormSchema, setInitialFormSchema] = useState(); + const [initialBasicAuthSecretName, setInitialBasicAuthSecretName] = useState(''); const helmAction: HelmActionType = initialChartURL ? HelmActionType.Create : HelmActionType.Upgrade; @@ -101,6 +102,9 @@ const HelmInstallUpgradePage: FC = () => { const valuesYAML = releaseValues || chartValues; const valuesJSON = (res?.config || chart?.values) ?? {}; const valuesSchema = chart?.schema && JSON.parse(atob(chart?.schema)); + const basicAuthSecretName = + chart?.metadata?.annotations?.['helm.openshift.io/auth-secret'] ?? ''; + setInitialBasicAuthSecretName(basicAuthSecretName); setInitialYamlData(valuesYAML); setInitialFormData(valuesJSON); setInitialFormSchema(valuesSchema); @@ -120,6 +124,8 @@ const HelmInstallUpgradePage: FC = () => { }; }, [config.helmReleaseApi, helmAction]); + const isURLInstall = chartData?.metadata?.annotations?.installation === 'url_install'; + const initialValues: HelmInstallUpgradeFormData = { releaseName: initialReleaseName || helmChartName || '', chartURL: initialChartURL, @@ -133,6 +139,8 @@ const HelmInstallUpgradePage: FC = () => { formData: initialFormData, formSchema: initialFormSchema, editorType: initialFormSchema ? EditorType.Form : EditorType.YAML, + basicAuthSecretName: initialBasicAuthSecretName, + isURLInstall, }; const handleSubmit = (values, actions) => { @@ -143,6 +151,7 @@ const HelmInstallUpgradePage: FC = () => { yamlData, formData, editorType, + basicAuthSecretName, }: HelmInstallUpgradeFormData = values; let valuesObj; @@ -182,6 +191,7 @@ const HelmInstallUpgradePage: FC = () => { ...(chartURL ? { chart_url: chartURL } : {}), // eslint-disable-line @typescript-eslint/naming-convention ...(indexEntry ? { indexEntry } : { indexEntry: chartIndexEntry }), ...(valuesObj ? { values: valuesObj } : {}), + ...(values.isURLInstall ? { basic_auth_secret_name: basicAuthSecretName } : {}), // eslint-disable-line @typescript-eslint/naming-convention }; return config diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx new file mode 100644 index 00000000000..fa9e9716959 --- /dev/null +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx @@ -0,0 +1,188 @@ +import { useCallback, useState } from 'react'; +import type { FormEvent } from 'react'; +import { + Button, + ButtonVariant, + Form, + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + Modal, + ModalBody, + ModalHeader, + ModalVariant, + TextInput, +} from '@patternfly/react-core'; +import { useTranslation } from 'react-i18next'; +import type { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; +import { SecretModel } from '@console/internal/models'; +import { k8sCreate } from '@console/internal/module/k8s'; +import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; + +interface HelmCreateBasicAuthSecretModalProps { + namespace: string; + save?: (name: string) => void; + onClose?: () => void; +} + +const HelmCreateBasicAuthSecretModal: OverlayComponent = ({ + closeOverlay, + namespace, + save, + onClose, +}) => { + const { t } = useTranslation('helm-plugin'); + const [secretName, setSecretName] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [inProgress, setInProgress] = useState(false); + const [errorMessage, setErrorMessage] = useState(); + + const isCreateDisabled = !secretName.trim() || !username.trim() || !password; + + const closeModal = (force = false) => { + if (inProgress && !force) { + return; + } + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + onClose?.(); + closeOverlay(); + }; + + const handleCreate = async () => { + if (isCreateDisabled || inProgress) { + return; + } + + setInProgress(true); + setErrorMessage(undefined); + + try { + await k8sCreate(SecretModel, { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: secretName.trim(), + namespace, + }, + type: 'kubernetes.io/basic-auth', + stringData: { + ...(username ? { username } : {}), + password, + }, + }); + const createdSecretName = secretName.trim(); + closeModal(true); + // Keep form update separate so a parent callback failure cannot block modal close. + save?.(createdSecretName); + } catch (err) { + const message = err instanceof Error ? err.message : t('Failed to create Secret.'); + setErrorMessage(message); + } finally { + setInProgress(false); + } + }; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + await handleCreate(); + }; + + return ( + closeModal(true)} + title={t('Create authentication Secret')} + variant={ModalVariant.medium} + > + + +
+ + setSecretName(value)} + isRequired + /> + + + {t('Unique name of the Secret.')} + + + + + + setUsername(value)} + /> + + + {t('Username for OCI/HTTP(S) authentication.')} + + + + + + setPassword(value)} + isRequired + /> + + + + {t('Password or token for OCI/HTTP(S) authentication.')} + + + + +
+
+ + + + +
+ ); +}; + +export type HelmCreateBasicAuthSecretModalLauncher = ( + props?: HelmCreateBasicAuthSecretModalProps, +) => void; + +export const useHelmCreateBasicAuthSecretModal = (): HelmCreateBasicAuthSecretModalLauncher => { + const launcher = useOverlay(); + return useCallback((props) => launcher(HelmCreateBasicAuthSecretModal, props), [launcher]); +}; diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx index ec31445c752..2d86263dd8b 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { TextInputTypes, Grid, GridItem } from '@patternfly/react-core'; import type { FormikProps } from 'formik'; import * as fuzzy from 'fuzzysearch'; @@ -11,6 +11,7 @@ import { FormFooter } from '@console/shared/src/components/form-utils/FormFooter import { FormHeader } from '@console/shared/src/components/form-utils/FormHeader'; import { InputField } from '@console/shared/src/components/formik-fields/InputField'; import { ResourceDropdownField } from '@console/shared/src/components/formik-fields/ResourceDropdownField'; +import { useHelmCreateBasicAuthSecretModal } from './HelmCreateBasicAuthSecretModal'; import type { HelmURLChartFormData } from './types'; import { useSecretResources } from './useSecretResources'; @@ -32,6 +33,34 @@ const HelmURLChartForm: FC & HelmURLChartFormP setFieldError, }) => { const { t } = useTranslation('helm-plugin'); + const launchHelmCreateBasicAuthSecretModal = useHelmCreateBasicAuthSecretModal(); + const [isCreateSecretModalOpen, setIsCreateSecretModalOpen] = useState(false); + + const CREATE_SECRET_KEY = 'create-secret'; + + const handleSecretSave = (name: string) => { + setFieldValue('basicAuthSecretName', name); + }; + + const handleSecretChange = (key: string) => { + if (key === CREATE_SECRET_KEY && !isCreateSecretModalOpen) { + // ResourceDropdownField writes the selected key to form state after this callback. + // Defer restoring the previous secret so "create-secret" is not persisted. + window.setTimeout( + () => setFieldValue('basicAuthSecretName', values.basicAuthSecretName || ''), + 0, + ); + setIsCreateSecretModalOpen(true); + launchHelmCreateBasicAuthSecretModal({ + namespace, + save: (name) => { + handleSecretSave(name); + setIsCreateSecretModalOpen(false); + }, + onClose: () => setIsCreateSecretModalOpen(false), + }); + } + }; const autocompleteFilter = (strText: string, item: any): boolean => fuzzy(strText, item?.props?.name); @@ -138,8 +167,15 @@ const HelmURLChartForm: FC & HelmURLChartFormP placeholder={t('Select a secret')} showBadge autocompleteFilter={autocompleteFilter} + actionItems={[ + { + actionTitle: t('Create Secret'), + actionKey: CREATE_SECRET_KEY, + }, + ]} + onChange={handleSecretChange} helpText={t( - 'A secret with "username" and "password" keys for OCI/HTTP(S) authentication', + 'Secret with "username" and "password" keys for OCI/HTTP(S) authentication.', )} /> From 1724ee079f2dbba7a39de0ae0306e7e9cfb0cd45 Mon Sep 17 00:00:00 2001 From: sowmya-sl Date: Tue, 21 Jul 2026 17:07:33 +0530 Subject: [PATCH 3/7] HELM-763: Address review feedback for auth secret support - Make 401/unauthorized error detection case-insensitive across get_chart, install_chart, and upgrade_release - Capitalize 'Basic' in 'Secret for Basic authentication' label - Extract secretName.trim() to a single variable in the modal - Remove duplicate locale key for 'Secret for Basic authentication' Co-authored-by: Cursor --- frontend/packages/helm-plugin/locales/en/helm-plugin.json | 3 +-- .../forms/install-upgrade/HelmInstallUpgradeForm.tsx | 2 +- .../forms/url-chart/HelmCreateBasicAuthSecretModal.tsx | 7 ++++--- pkg/helm/actions/get_chart.go | 2 +- pkg/helm/actions/install_chart.go | 2 +- pkg/helm/actions/upgrade_release.go | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/packages/helm-plugin/locales/en/helm-plugin.json b/frontend/packages/helm-plugin/locales/en/helm-plugin.json index 24437966a4e..e285b0b1d7e 100644 --- a/frontend/packages/helm-plugin/locales/en/helm-plugin.json +++ b/frontend/packages/helm-plugin/locales/en/helm-plugin.json @@ -52,7 +52,7 @@ "The Helm Chart is currently unavailable. {{chartError}}": "The Helm Chart is currently unavailable. {{chartError}}", "Release name": "Release name", "A unique name for the Helm Release.": "A unique name for the Helm Release.", - "Secret for basic authentication": "Secret for basic authentication", + "Secret for Basic authentication": "Secret for Basic authentication", "None": "None", "Select a secret": "Select a secret", "Create Secret": "Create Secret", @@ -80,7 +80,6 @@ "The OCI URL or HTTP/HTTPS tar file for the Helm chart; for example - oci://registry.example.com/charts/mychart or https://example.com/chart-1.0.0.tgz.": "The OCI URL or HTTP/HTTPS tar file for the Helm chart; for example - oci://registry.example.com/charts/mychart or https://example.com/chart-1.0.0.tgz.", "Unique name for Helm release.": "Unique name for Helm release.", "The version of chart to install.": "The version of chart to install.", - "Secret for Basic authentication": "Secret for Basic authentication", "Next": "Next", "Install Helm chart from Helm registry.": "Install Helm chart from Helm registry.", "Helm release": "Helm release", diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx index 185e40becc7..aa576a61331 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx @@ -222,7 +222,7 @@ const HelmInstallUpgradeForm: FC< Date: Thu, 23 Jul 2026 16:34:23 +0530 Subject: [PATCH 4/7] =?UTF-8?q?HELM-763:=20Address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20tests,=20shared=20hook,=20and=20a11y=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add unit tests for HelmCreateBasicAuthSecretModal covering field validation, k8sCreate shape, save callback, error state, and cancel - Add backend tests for explicit secret override, __none__ sentinel clearing, and credential lookup error propagation - Extract useBasicAuthSecretDropdown shared hook from duplicated logic in HelmInstallUpgradeForm and HelmURLChartForm - Hoist CREATE_SECRET_KEY and NONE_SECRET_KEY to module scope - Use NONE_SECRET_KEY constant in HelmInstallUpgradePage payload - Fix PF6 Modal: remove title prop, add aria-labelledby + labelId - Add missing isRequired on username TextInput for a11y parity - Export HelmCreateBasicAuthSecretModal for test imports Co-authored-by: Cursor --- .../HelmInstallUpgradeForm.tsx | 47 ++---- .../HelmInstallUpgradePage.tsx | 10 +- .../HelmCreateBasicAuthSecretModal.tsx | 10 +- .../forms/url-chart/HelmURLChartForm.tsx | 37 +---- .../HelmCreateBasicAuthSecretModal.spec.tsx | 121 +++++++++++++++ .../url-chart/useBasicAuthSecretDropdown.ts | 55 +++++++ pkg/helm/actions/upgrade_release_test.go | 138 ++++++++++++++++++ 7 files changed, 349 insertions(+), 69 deletions(-) create mode 100644 frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmCreateBasicAuthSecretModal.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx index aa576a61331..c09a3c6c443 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx @@ -1,5 +1,5 @@ import type { ReactNode, FC } from 'react'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { TextInputTypes, Grid, GridItem, Button, Alert } from '@patternfly/react-core'; import type { FormikProps } from 'formik'; import * as fuzzy from 'fuzzysearch'; @@ -21,7 +21,11 @@ import type { EditorType } from '@console/shared/src/components/synced-editor/ed import type { HelmChart, HelmActionConfigType } from '../../../types/helm-types'; import { HelmActionType } from '../../../types/helm-types'; import { helmActionString } from '../../../utils/helm-utils'; -import { useHelmCreateBasicAuthSecretModal } from '../url-chart/HelmCreateBasicAuthSecretModal'; +import { + useBasicAuthSecretDropdown, + CREATE_SECRET_KEY, + NONE_SECRET_KEY, +} from '../url-chart/useBasicAuthSecretDropdown'; import { useSecretResources } from '../url-chart/useSecretResources'; import HelmChartVersionDropdown from './HelmChartVersionDropdown'; import { useHelmReadmeModalLauncher } from './HelmReadmeModal'; @@ -77,39 +81,12 @@ const HelmInstallUpgradeForm: FC< setFieldValue, }) => { const { t } = useTranslation('helm-plugin'); - const launchHelmCreateBasicAuthSecretModal = useHelmCreateBasicAuthSecretModal(); - const [isCreateSecretModalOpen, setIsCreateSecretModalOpen] = useState(false); - - const CREATE_SECRET_KEY = 'create-secret'; - const NONE_SECRET_KEY = '__none__'; - - const handleSecretSave = (name: string) => { - setFieldValue('basicAuthSecretName', name); - }; - - const handleSecretChange = (key: string) => { - if (key === NONE_SECRET_KEY) { - window.setTimeout(() => setFieldValue('basicAuthSecretName', NONE_SECRET_KEY), 0); - return; - } - if (key === CREATE_SECRET_KEY && !isCreateSecretModalOpen) { - // ResourceDropdownField writes the selected key to form state after this callback. - // Defer restoring the previous secret so "create-secret" is not persisted. - window.setTimeout( - () => setFieldValue('basicAuthSecretName', values.basicAuthSecretName || ''), - 0, - ); - setIsCreateSecretModalOpen(true); - launchHelmCreateBasicAuthSecretModal({ - namespace, - save: (name) => { - handleSecretSave(name); - setIsCreateSecretModalOpen(false); - }, - onClose: () => setIsCreateSecretModalOpen(false), - }); - } - }; + const { handleSecretChange } = useBasicAuthSecretDropdown({ + namespace, + currentSecretName: values.basicAuthSecretName, + setFieldValue, + supportNone: true, + }); const { chartName, chartVersion, chartReadme, formData, formSchema, editorType } = values; const { type: helmAction, title, subTitle } = helmActionConfig; const helmReadmeModalLauncher = useHelmReadmeModalLauncher({ readme: chartReadme }); diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx index c850ba51daa..59389595c71 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx @@ -33,6 +33,7 @@ import { isGoingToTopology, } from '../../../utils/helm-utils'; import { getHelmActionValidationSchema } from '../../../utils/helm-validation-utils'; +import { NONE_SECRET_KEY } from '../url-chart/useBasicAuthSecretDropdown'; import HelmChartMetaDescription from './HelmChartMetaDescription'; import type { HelmInstallUpgradeFormData } from './HelmInstallUpgradeForm'; import HelmInstallUpgradeForm from './HelmInstallUpgradeForm'; @@ -191,7 +192,14 @@ const HelmInstallUpgradePage: FC = () => { ...(chartURL ? { chart_url: chartURL } : {}), // eslint-disable-line @typescript-eslint/naming-convention ...(indexEntry ? { indexEntry } : { indexEntry: chartIndexEntry }), ...(valuesObj ? { values: valuesObj } : {}), - ...(values.isURLInstall ? { basic_auth_secret_name: basicAuthSecretName } : {}), // eslint-disable-line @typescript-eslint/naming-convention + ...(values.isURLInstall + ? { + basic_auth_secret_name: + helmAction === HelmActionType.Create && basicAuthSecretName === NONE_SECRET_KEY + ? '' + : basicAuthSecretName, + } + : {}), // eslint-disable-line @typescript-eslint/naming-convention }; return config diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx index e709f48f6f3..1d2410d2bd1 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx @@ -27,7 +27,7 @@ interface HelmCreateBasicAuthSecretModalProps { onClose?: () => void; } -const HelmCreateBasicAuthSecretModal: OverlayComponent = ({ +export const HelmCreateBasicAuthSecretModal: OverlayComponent = ({ closeOverlay, namespace, save, @@ -97,10 +97,13 @@ const HelmCreateBasicAuthSecretModal: OverlayComponent closeModal(true)} - title={t('Create authentication Secret')} variant={ModalVariant.medium} + aria-labelledby="helm-create-auth-secret-modal-title" > - +
@@ -126,6 +129,7 @@ const HelmCreateBasicAuthSecretModal: OverlayComponent setUsername(value)} + isRequired /> diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx index 2d86263dd8b..5a178731888 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { TextInputTypes, Grid, GridItem } from '@patternfly/react-core'; import type { FormikProps } from 'formik'; import * as fuzzy from 'fuzzysearch'; @@ -11,8 +11,8 @@ import { FormFooter } from '@console/shared/src/components/form-utils/FormFooter import { FormHeader } from '@console/shared/src/components/form-utils/FormHeader'; import { InputField } from '@console/shared/src/components/formik-fields/InputField'; import { ResourceDropdownField } from '@console/shared/src/components/formik-fields/ResourceDropdownField'; -import { useHelmCreateBasicAuthSecretModal } from './HelmCreateBasicAuthSecretModal'; import type { HelmURLChartFormData } from './types'; +import { useBasicAuthSecretDropdown, CREATE_SECRET_KEY } from './useBasicAuthSecretDropdown'; import { useSecretResources } from './useSecretResources'; interface HelmURLChartFormProps { @@ -33,34 +33,11 @@ const HelmURLChartForm: FC & HelmURLChartFormP setFieldError, }) => { const { t } = useTranslation('helm-plugin'); - const launchHelmCreateBasicAuthSecretModal = useHelmCreateBasicAuthSecretModal(); - const [isCreateSecretModalOpen, setIsCreateSecretModalOpen] = useState(false); - - const CREATE_SECRET_KEY = 'create-secret'; - - const handleSecretSave = (name: string) => { - setFieldValue('basicAuthSecretName', name); - }; - - const handleSecretChange = (key: string) => { - if (key === CREATE_SECRET_KEY && !isCreateSecretModalOpen) { - // ResourceDropdownField writes the selected key to form state after this callback. - // Defer restoring the previous secret so "create-secret" is not persisted. - window.setTimeout( - () => setFieldValue('basicAuthSecretName', values.basicAuthSecretName || ''), - 0, - ); - setIsCreateSecretModalOpen(true); - launchHelmCreateBasicAuthSecretModal({ - namespace, - save: (name) => { - handleSecretSave(name); - setIsCreateSecretModalOpen(false); - }, - onClose: () => setIsCreateSecretModalOpen(false), - }); - } - }; + const { handleSecretChange } = useBasicAuthSecretDropdown({ + namespace, + currentSecretName: values.basicAuthSecretName, + setFieldValue, + }); const autocompleteFilter = (strText: string, item: any): boolean => fuzzy(strText, item?.props?.name); diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmCreateBasicAuthSecretModal.spec.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmCreateBasicAuthSecretModal.spec.tsx new file mode 100644 index 00000000000..c0b5151109a --- /dev/null +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmCreateBasicAuthSecretModal.spec.tsx @@ -0,0 +1,121 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SecretModel } from '@console/internal/models'; +import { k8sCreate } from '@console/internal/module/k8s'; +import { HelmCreateBasicAuthSecretModal } from '../HelmCreateBasicAuthSecretModal'; + +jest.mock('@console/internal/module/k8s', () => ({ + ...jest.requireActual('@console/internal/module/k8s'), + k8sCreate: jest.fn(), +})); + +const mockCloseOverlay = jest.fn(); +const mockSave = jest.fn(); +const mockOnClose = jest.fn(); + +const defaultProps = { + closeOverlay: mockCloseOverlay, + namespace: 'test-ns', + save: mockSave, + onClose: mockOnClose, +}; + +describe('HelmCreateBasicAuthSecretModal', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the modal with all form fields and buttons', () => { + render(); + + expect(screen.getByText('Secret name')).toBeVisible(); + expect(screen.getByText('Secret username')).toBeVisible(); + expect(screen.getByText('Secret password or token')).toBeVisible(); + expect(screen.getByRole('button', { name: 'Create' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible(); + }); + + it('should disable the Create button when required fields are empty', () => { + render(); + + const createButton = screen.getByRole('button', { name: 'Create' }); + expect(createButton).toBeDisabled(); + }); + + it('should enable the Create button when all required fields are filled', async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByTestId('helm-secret-name'), 'my-secret'); + await user.type(screen.getByTestId('helm-secret-username'), 'admin'); + await user.type(screen.getByTestId('helm-secret-password'), 'pass123'); + + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + }); + + it('should call k8sCreate with correct Secret shape and invoke save callback on success', async () => { + const user = userEvent.setup(); + (k8sCreate as jest.Mock).mockResolvedValue({}); + render(); + + await user.type(screen.getByTestId('helm-secret-name'), ' my-secret '); + await user.type(screen.getByTestId('helm-secret-username'), 'admin'); + await user.type(screen.getByTestId('helm-secret-password'), 'pass123'); + await user.click(screen.getByRole('button', { name: 'Create' })); + + await waitFor(() => { + expect(k8sCreate).toHaveBeenCalledWith(SecretModel, { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'my-secret', + namespace: 'test-ns', + }, + type: 'kubernetes.io/basic-auth', + stringData: { + username: 'admin', + password: 'pass123', + }, + }); + }); + + expect(mockSave).toHaveBeenCalledWith('my-secret'); + expect(mockCloseOverlay).toHaveBeenCalled(); + }); + + it('should display error message when k8sCreate fails', async () => { + const user = userEvent.setup(); + (k8sCreate as jest.Mock).mockRejectedValue(new Error('Secret already exists')); + render(); + + await user.type(screen.getByTestId('helm-secret-name'), 'my-secret'); + await user.type(screen.getByTestId('helm-secret-username'), 'admin'); + await user.type(screen.getByTestId('helm-secret-password'), 'pass123'); + await user.click(screen.getByRole('button', { name: 'Create' })); + + expect(await screen.findByText('Secret already exists')).toBeVisible(); + expect(mockSave).not.toHaveBeenCalled(); + expect(mockCloseOverlay).not.toHaveBeenCalled(); + }); + + it('should call closeOverlay and onClose when Cancel button is clicked', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'Cancel' })); + + expect(mockCloseOverlay).toHaveBeenCalled(); + expect(mockOnClose).toHaveBeenCalled(); + expect(mockSave).not.toHaveBeenCalled(); + }); + + it('should keep Create button disabled when only secret name and password are filled but username is empty', async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByTestId('helm-secret-name'), 'my-secret'); + await user.type(screen.getByTestId('helm-secret-password'), 'pass123'); + + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + }); +}); diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts b/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts new file mode 100644 index 00000000000..f788b387e82 --- /dev/null +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts @@ -0,0 +1,55 @@ +import { useState, useCallback } from 'react'; +import { useHelmCreateBasicAuthSecretModal } from './HelmCreateBasicAuthSecretModal'; + +const CREATE_SECRET_KEY = 'create-secret'; +const NONE_SECRET_KEY = '__none__'; + +export { CREATE_SECRET_KEY, NONE_SECRET_KEY }; + +interface UseBasicAuthSecretDropdownOptions { + namespace: string; + currentSecretName: string; + setFieldValue: (field: string, value: string) => void; + supportNone?: boolean; +} + +export const useBasicAuthSecretDropdown = ({ + namespace, + currentSecretName, + setFieldValue, + supportNone = false, +}: UseBasicAuthSecretDropdownOptions) => { + const launchModal = useHelmCreateBasicAuthSecretModal(); + const [isCreateSecretModalOpen, setIsCreateSecretModalOpen] = useState(false); + + const handleSecretChange = useCallback( + (key: string) => { + if (supportNone && key === NONE_SECRET_KEY) { + window.setTimeout(() => setFieldValue('basicAuthSecretName', NONE_SECRET_KEY), 0); + return; + } + if (key === CREATE_SECRET_KEY && !isCreateSecretModalOpen) { + window.setTimeout(() => setFieldValue('basicAuthSecretName', currentSecretName || ''), 0); + setIsCreateSecretModalOpen(true); + launchModal({ + namespace, + save: (name) => { + setFieldValue('basicAuthSecretName', name); + setIsCreateSecretModalOpen(false); + }, + onClose: () => setIsCreateSecretModalOpen(false), + }); + } + }, + [ + supportNone, + isCreateSecretModalOpen, + launchModal, + namespace, + currentSecretName, + setFieldValue, + ], + ); + + return { handleSecretChange }; +}; diff --git a/pkg/helm/actions/upgrade_release_test.go b/pkg/helm/actions/upgrade_release_test.go index ab969ccfc53..ed6dacff907 100644 --- a/pkg/helm/actions/upgrade_release_test.go +++ b/pkg/helm/actions/upgrade_release_test.go @@ -791,3 +791,141 @@ func TestUpgradeAfterURLInstallWithSecrets(t *testing.T) { }) } } + +func TestUpgradeAsyncExplicitSecretOverridesAnnotation(t *testing.T) { + store := storage.Init(driver.NewMemory()) + actionConfig := &action.Configuration{ + RESTClientGetter: FakeConfig{}, + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, + Capabilities: common.DefaultCapabilities, + } + + // Only create the override secret, not the annotation secret. + // If the code incorrectly uses the annotation value ("missing-secret"), + // GetUserCredentials will fail because that secret doesn't exist. + overrideSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "new-secret", Namespace: "test-ns"}, + Data: map[string][]byte{username: []byte("new-user"), password: []byte("new-pass")}, + } + clientInterface := k8sfake.NewSimpleClientset(overrideSecret) + coreClient := clientInterface.CoreV1() + registryClient, err := GetDefaultOCIRegistry() + require.NoError(t, err) + actionConfig.RegistryClient = registryClient + dynamicClient := K8sDynamicClientFromCRs() + + installRelease := releasev1.Release{ + Name: "override-test", + Namespace: "test-ns", + Info: &releasev1.Info{FirstDeployed: helmTime.Time{}, Status: "deployed"}, + Version: 1, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "mychart", + Version: "0.1.0", + Annotations: map[string]string{ + "chart_url": "http://localhost:8181/charts/mychart-0.1.0.tgz", + helmAuthSecretAnnotation: "missing-secret", + }, + }, + }, + } + store.Create(&installRelease) + + // Pass "new-secret" explicitly — this should override "missing-secret" from the annotation. + // The chart fetch will fail with 401 (fake credentials), but the key assertion is that + // it does NOT fail with "failed to get user credentials Secret missing-secret" which + // would indicate the annotation value was used instead of the explicit override. + _, err = UpgradeReleaseAsync("test-ns", "override-test", "", nil, actionConfig, dynamicClient, coreClient, true, "", "new-secret") + require.Error(t, err) + require.NotContains(t, err.Error(), "missing-secret", "should use the explicit secret, not the annotation value") +} + +func TestUpgradeAsyncNoneSentinelClearsAnnotationSecret(t *testing.T) { + store := storage.Init(driver.NewMemory()) + actionConfig := &action.Configuration{ + RESTClientGetter: FakeConfig{}, + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, + Capabilities: common.DefaultCapabilities, + } + + // Create the annotation secret — if __none__ doesn't work, the code would + // fall back to this secret and NOT produce a 401 auth hint. + annotationSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "old-secret", Namespace: "test-ns"}, + Data: map[string][]byte{username: []byte("user"), password: []byte("pass")}, + } + clientInterface := k8sfake.NewSimpleClientset(annotationSecret) + coreClient := clientInterface.CoreV1() + registryClient, err := GetDefaultOCIRegistry() + require.NoError(t, err) + actionConfig.RegistryClient = registryClient + dynamicClient := K8sDynamicClientFromCRs() + + installRelease := releasev1.Release{ + Name: "none-test", + Namespace: "test-ns", + Info: &releasev1.Info{FirstDeployed: helmTime.Time{}, Status: "deployed"}, + Version: 1, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "mychart", + Version: "0.1.0", + Annotations: map[string]string{ + "chart_url": "http://localhost:8181/charts/mychart-0.1.0.tgz", + helmAuthSecretAnnotation: "old-secret", + }, + }, + }, + } + store.Create(&installRelease) + + // "__none__" sentinel — should clear the secret and NOT fall back to the annotation. + // The chart fetch will fail with 401 since no auth is applied, proving the sentinel worked. + _, err = UpgradeReleaseAsync("test-ns", "none-test", "", nil, actionConfig, dynamicClient, coreClient, true, "", "__none__") + require.Error(t, err) + require.Contains(t, err.Error(), "registry requires authentication", + "should get 401 hint because __none__ cleared the secret and skipped annotation fallback") +} + +func TestUpgradeAsyncGetUserCredentialsFailureReturnsError(t *testing.T) { + store := storage.Init(driver.NewMemory()) + actionConfig := &action.Configuration{ + RESTClientGetter: FakeConfig{}, + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, + Capabilities: common.DefaultCapabilities, + } + + // No secrets in the fake client — GetUserCredentials will fail + clientInterface := k8sfake.NewSimpleClientset() + coreClient := clientInterface.CoreV1() + registryClient, err := GetDefaultOCIRegistry() + require.NoError(t, err) + actionConfig.RegistryClient = registryClient + dynamicClient := K8sDynamicClientFromCRs() + + installRelease := releasev1.Release{ + Name: "err-test", + Namespace: "test-ns", + Info: &releasev1.Info{FirstDeployed: helmTime.Time{}, Status: "deployed"}, + Version: 1, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "mychart", + Version: "0.1.0", + Annotations: map[string]string{ + "chart_url": "http://localhost:8181/charts/mychart-0.1.0.tgz", + }, + }, + }, + } + store.Create(&installRelease) + + // Pass a secret name that doesn't exist — should return an error, not silently log + _, err = UpgradeReleaseAsync("test-ns", "err-test", "", nil, actionConfig, dynamicClient, coreClient, true, "", "nonexistent-secret") + require.Error(t, err) + require.Contains(t, err.Error(), "failed to get user credentials Secret nonexistent-secret") +} From 03abe52beff77af0374cb3627512b5289d1f136d Mon Sep 17 00:00:00 2001 From: sowmya-sl Date: Thu, 23 Jul 2026 18:23:57 +0530 Subject: [PATCH 5/7] =?UTF-8?q?HELM-763:=20Fix=20i18n=20locale=20file=20?= =?UTF-8?q?=E2=80=94=20deduplicate=20and=20sort=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix i18n locale file. Also add guard for rel.Chart.Metadata reference. --- .../helm-plugin/locales/en/helm-plugin.json | 79 +++---------------- pkg/helm/actions/upgrade_release.go | 12 ++- 2 files changed, 19 insertions(+), 72 deletions(-) diff --git a/frontend/packages/helm-plugin/locales/en/helm-plugin.json b/frontend/packages/helm-plugin/locales/en/helm-plugin.json index e285b0b1d7e..a2e077a0cd4 100644 --- a/frontend/packages/helm-plugin/locales/en/helm-plugin.json +++ b/frontend/packages/helm-plugin/locales/en/helm-plugin.json @@ -39,77 +39,11 @@ "Complete the form to create a Helm release. The Helm chart authors might have provided some default values.": "Complete the form to create a Helm release. The Helm chart authors might have provided some default values.", "Configure Helm release": "Configure Helm release", "Create": "Create", - "Create Helm Chart Repository": "Create Helm Chart Repository", - "Change chart version?": "Change chart version?", - "Are you sure you want to change the chart version from <2>{{currentVersion}} to <4>{{newVersion}}? ": "Are you sure you want to change the chart version from <2>{{currentVersion}} to <4>{{newVersion}}? ", - "Values from your current release are merged with the new chart's defaults. Review the YAML or form before upgrading.": "Values from your current release are merged with the new chart's defaults. Review the YAML or form before upgrading.", - "Proceed": "Proceed", - "Select the version to upgrade to.": "Select the version to upgrade to.", - "No versions available": "No versions available", - "Helm Release": "Helm Release", - "For more information on the chart, refer to this <2>README": "For more information on the chart, refer to this <2>README", - "Helm Chart cannot be installed": "Helm Chart cannot be installed", - "The Helm Chart is currently unavailable. {{chartError}}": "The Helm Chart is currently unavailable. {{chartError}}", - "Release name": "Release name", - "A unique name for the Helm Release.": "A unique name for the Helm Release.", - "Secret for Basic authentication": "Secret for Basic authentication", - "None": "None", - "Select a secret": "Select a secret", - "Create Secret": "Create Secret", - "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.": "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.", - "Secret \"{{secretName}}\" was not found in this namespace. Select an existing secret or create a new one.": "Secret \"{{secretName}}\" was not found in this namespace. Select an existing secret or create a new one.", - "Helm release is not configurable since the Helm Chart doesn't define any values.": "Helm release is not configurable since the Helm Chart doesn't define any values.", - "Errors in the form data.": "Errors in the form data.", - "Invalid Form Schema - {{errorText}}": "Invalid Form Schema - {{errorText}}", - "Invalid YAML - {{errorText}}": "Invalid YAML - {{errorText}}", - "Select the version to rollback <1>{{releaseName}} to, from the table below:": "Select the version to rollback <1>{{releaseName}} to, from the table below:", - "Select": "Select", - "Must be a valid OCI URL or a valid HTTP/HTTPS tar file; for example - oci://registry.example.com/chart, https://example.com/chart-1.0.0.tgz.": "Must be a valid OCI URL or a valid HTTP/HTTPS tar file; for example - oci://registry.example.com/chart, https://example.com/chart-1.0.0.tgz.", - "Failed to create Secret.": "Failed to create Secret.", "Create authentication Secret": "Create authentication Secret", - "Secret name": "Secret name", - "Unique name of the Secret.": "Unique name of the Secret.", - "Secret username": "Secret username", - "Username for OCI/HTTP(S) authentication.": "Username for OCI/HTTP(S) authentication.", - "Secret password or token": "Secret password or token", - "Password or token for OCI/HTTP(S) authentication.": "Password or token for OCI/HTTP(S) authentication.", - "Invalid chart URL format.": "Invalid chart URL format.", - "Install Helm chart from URL": "Install Helm chart from URL", - "To install a Helm chart, enter the chart URL - Open Container Initiative (OCI) URL or HTTP/HTTPS tar file and version.": "To install a Helm chart, enter the chart URL - Open Container Initiative (OCI) URL or HTTP/HTTPS tar file and version.", - "Chart URL": "Chart URL", - "The OCI URL or HTTP/HTTPS tar file for the Helm chart; for example - oci://registry.example.com/charts/mychart or https://example.com/chart-1.0.0.tgz.": "The OCI URL or HTTP/HTTPS tar file for the Helm chart; for example - oci://registry.example.com/charts/mychart or https://example.com/chart-1.0.0.tgz.", - "Unique name for Helm release.": "Unique name for Helm release.", - "The version of chart to install.": "The version of chart to install.", - "Next": "Next", - "Install Helm chart from Helm registry.": "Install Helm chart from Helm registry.", - "Helm release": "Helm release", - "Complete the form to create a Helm release. The Helm chart authors might have provided some default values.": "Complete the form to create a Helm release. The Helm chart authors might have provided some default values.", - "Configure Helm release": "Configure Helm release", - "Version": "Version", - "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication": "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication", - "Install": "Install", - "Back": "Back", - "Namespace": "Namespace", - "Unable to load Helm Releases": "Unable to load Helm Releases", - "Filter by status": "Filter by status", - "No Helm Releases found": "No Helm Releases found", - "Browse the catalog to discover available Helm Charts": "Browse the catalog to discover available Helm Charts", - "Helm chart URL": "Helm chart URL", - "Repositories": "Repositories", - "Select a Project to view its details<1>.": "Select a Project to view its details<1>.", - "All Namespaces": "All Namespaces", - "True": "True", - "False": "False", - "Display Name": "Display Name", - "Disabled": "Disabled", - "Repo URL": "Repo URL", - "Helm Repositories": "Helm Repositories", - "Helm release is {{status}}": "Helm release is {{status}}", - "Unable to find resource for {{helmLabel}}": "Unable to find resource for {{helmLabel}}", - " / App Version {{appVersion}}": " / App Version {{appVersion}}", - " (Provided by {{chartRepoName}})": " (Provided by {{chartRepoName}})", + "Create Helm Chart Repository": "Create Helm Chart Repository", "Create Helm Release": "Create Helm Release", "Create ProjectHelmChartRepository": "Create ProjectHelmChartRepository", + "Create Secret": "Create Secret", "Created": "Created", "Delete": "Delete", "Delete Helm Release": "Delete Helm Release", @@ -123,6 +57,7 @@ "Edit Helm Chart Repository": "Edit Helm Chart Repository", "Edit ProjectHelmChartRepository": "Edit ProjectHelmChartRepository", "Errors in the form data.": "Errors in the form data.", + "Failed to create Secret.": "Failed to create Secret.", "False": "False", "Filter by status": "Filter by status", "For more information on the chart, refer to this <2>README": "For more information on the chart, refer to this <2>README", @@ -168,6 +103,7 @@ "None": "None", "of": "of", "Partner": "Partner", + "Password or token for OCI/HTTP(S) authentication.": "Password or token for OCI/HTTP(S) authentication.", "per page": "per page", "Please enter a URL that is less then 2048 characters.": "Please enter a URL that is less then 2048 characters.", "Pods": "Pods", @@ -194,7 +130,12 @@ "Rollback to Revision {{revision}}": "Rollback to Revision {{revision}}", "Save": "Save", "Scope type": "Scope type", + "Secret \"{{secretName}}\" was not found in this namespace. Select an existing secret or create a new one.": "Secret \"{{secretName}}\" was not found in this namespace. Select an existing secret or create a new one.", "Secret for Basic authentication": "Secret for Basic authentication", + "Secret name": "Secret name", + "Secret password or token": "Secret password or token", + "Secret username": "Secret username", + "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.": "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.", "Select": "Select", "Select a Project to view its details<1>.": "Select a Project to view its details<1>.", "Select a secret": "Select a secret", @@ -222,6 +163,7 @@ "Unable to load Helm Release history": "Unable to load Helm Release history", "Unable to load Helm Releases": "Unable to load Helm Releases", "Unique name for Helm release.": "Unique name for Helm release.", + "Unique name of the Secret.": "Unique name of the Secret.", "Unknown error submitting": "Unknown error submitting", "Update helm chart repository in the namespace.": "Update helm chart repository in the namespace.", "Update the helm chart repository.": "Update the helm chart repository.", @@ -231,6 +173,7 @@ "Upgrade by selecting a new chart version or manually changing YAML.": "Upgrade by selecting a new chart version or manually changing YAML.", "Upgrade Helm Release": "Upgrade Helm Release", "URL": "URL", + "Username for OCI/HTTP(S) authentication.": "Username for OCI/HTTP(S) authentication.", "Values from your current release are merged with the new chart's defaults. Review the YAML or form before upgrading.": "Values from your current release are merged with the new chart's defaults. Review the YAML or form before upgrading.", "Version": "Version" } diff --git a/pkg/helm/actions/upgrade_release.go b/pkg/helm/actions/upgrade_release.go index 040f44f62ee..7d75e9fdce5 100644 --- a/pkg/helm/actions/upgrade_release.go +++ b/pkg/helm/actions/upgrade_release.go @@ -112,8 +112,10 @@ func UpgradeRelease( ch.Metadata.Annotations = make(map[string]string) } ch.Metadata.Annotations["chart_url"] = chartUrl - if inst, ok := rel.Chart.Metadata.Annotations["installation"]; ok { - ch.Metadata.Annotations["installation"] = inst + if rel.Chart.Metadata != nil && rel.Chart.Metadata.Annotations != nil { + if inst, ok := rel.Chart.Metadata.Annotations["installation"]; ok { + ch.Metadata.Annotations["installation"] = inst + } } } @@ -260,8 +262,10 @@ func UpgradeReleaseAsync( } if chartUrl != "" { ch.Metadata.Annotations["chart_url"] = chartUrl - if inst, ok := rel.Chart.Metadata.Annotations["installation"]; ok { - ch.Metadata.Annotations["installation"] = inst + if rel.Chart.Metadata != nil && rel.Chart.Metadata.Annotations != nil { + if inst, ok := rel.Chart.Metadata.Annotations["installation"]; ok { + ch.Metadata.Annotations["installation"] = inst + } } addAuthSecretAnnotation(ch, auth_secret) } From cdb0587fc41a4eeda80be6c508242879de397348 Mon Sep 17 00:00:00 2001 From: sowmya-sl Date: Fri, 24 Jul 2026 16:40:26 +0530 Subject: [PATCH 6/7] HELM-763: Simplify shared hook and harden secret name handling - Remove redundant supportNone flag from useBasicAuthSecretDropdown; the None action is only available when the form adds it to actionItems - Use i18n interpolation for username/password Secret key names to prevent translators from accidentally localizing them - Reject __none__ as a secret name in the create modal to prevent collision with the backend sentinel value - Remove trailing period from 'Unique name of the Secret' helper text Co-authored-by: Cursor --- .../helm-plugin/locales/en/helm-plugin.json | 5 +++-- .../install-upgrade/HelmInstallUpgradeForm.tsx | 4 ++-- .../url-chart/HelmCreateBasicAuthSecretModal.tsx | 9 ++++++++- .../components/forms/url-chart/HelmURLChartForm.tsx | 3 ++- .../forms/url-chart/useBasicAuthSecretDropdown.ts | 13 ++----------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend/packages/helm-plugin/locales/en/helm-plugin.json b/frontend/packages/helm-plugin/locales/en/helm-plugin.json index a2e077a0cd4..5e8904b166d 100644 --- a/frontend/packages/helm-plugin/locales/en/helm-plugin.json +++ b/frontend/packages/helm-plugin/locales/en/helm-plugin.json @@ -135,7 +135,7 @@ "Secret name": "Secret name", "Secret password or token": "Secret password or token", "Secret username": "Secret username", - "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.": "Secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication.", + "Secret with \"{{username}}\" and \"{{password}}\" keys for OCI/HTTP(S) authentication.": "Secret with \"{{username}}\" and \"{{password}}\" keys for OCI/HTTP(S) authentication.", "Select": "Select", "Select a Project to view its details<1>.": "Select a Project to view its details<1>.", "Select a secret": "Select a secret", @@ -155,6 +155,7 @@ "The repository name cannot exceed than 2048 characters.": "The repository name cannot exceed than 2048 characters.", "The version of chart to install.": "The version of chart to install.", "This Helm Chart is provided by a trusted partner and has been verified for ease of integration.": "This Helm Chart is provided by a trusted partner and has been verified for ease of integration.", + "This name is reserved. Choose a different name.": "This name is reserved. Choose a different name.", "TLS Client config": "TLS Client config", "To install a Helm chart, enter the chart URL - Open Container Initiative (OCI) URL or HTTP/HTTPS tar file and version.": "To install a Helm chart, enter the chart URL - Open Container Initiative (OCI) URL or HTTP/HTTPS tar file and version.", "True": "True", @@ -163,7 +164,7 @@ "Unable to load Helm Release history": "Unable to load Helm Release history", "Unable to load Helm Releases": "Unable to load Helm Releases", "Unique name for Helm release.": "Unique name for Helm release.", - "Unique name of the Secret.": "Unique name of the Secret.", + "Unique name of the Secret": "Unique name of the Secret", "Unknown error submitting": "Unknown error submitting", "Update helm chart repository in the namespace.": "Update helm chart repository in the namespace.", "Update the helm chart repository.": "Update the helm chart repository.", diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx index c09a3c6c443..0a635e2e1c8 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx @@ -85,7 +85,6 @@ const HelmInstallUpgradeForm: FC< namespace, currentSecretName: values.basicAuthSecretName, setFieldValue, - supportNone: true, }); const { chartName, chartVersion, chartReadme, formData, formSchema, editorType } = values; const { type: helmAction, title, subTitle } = helmActionConfig; @@ -220,7 +219,8 @@ const HelmInstallUpgradeForm: FC< ]} onChange={handleSecretChange} helpText={t( - 'Secret with "username" and "password" keys for OCI/HTTP(S) authentication.', + 'Secret with "{{username}}" and "{{password}}" keys for OCI/HTTP(S) authentication.', + { username: 'username', password: 'password' }, )} /> {secretMissing && ( diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx index 1d2410d2bd1..59acadd6151 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmCreateBasicAuthSecretModal.tsx @@ -63,6 +63,13 @@ export const HelmCreateBasicAuthSecretModal: OverlayComponent - {t('Unique name of the Secret.')} + {t('Unique name of the Secret')} diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx index 5a178731888..657dad4a83c 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx @@ -152,7 +152,8 @@ const HelmURLChartForm: FC & HelmURLChartFormP ]} onChange={handleSecretChange} helpText={t( - 'Secret with "username" and "password" keys for OCI/HTTP(S) authentication.', + 'Secret with "{{username}}" and "{{password}}" keys for OCI/HTTP(S) authentication.', + { username: 'username', password: 'password' }, )} /> diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts b/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts index f788b387e82..782e7f388d9 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts @@ -10,21 +10,19 @@ interface UseBasicAuthSecretDropdownOptions { namespace: string; currentSecretName: string; setFieldValue: (field: string, value: string) => void; - supportNone?: boolean; } export const useBasicAuthSecretDropdown = ({ namespace, currentSecretName, setFieldValue, - supportNone = false, }: UseBasicAuthSecretDropdownOptions) => { const launchModal = useHelmCreateBasicAuthSecretModal(); const [isCreateSecretModalOpen, setIsCreateSecretModalOpen] = useState(false); const handleSecretChange = useCallback( (key: string) => { - if (supportNone && key === NONE_SECRET_KEY) { + if (key === NONE_SECRET_KEY) { window.setTimeout(() => setFieldValue('basicAuthSecretName', NONE_SECRET_KEY), 0); return; } @@ -41,14 +39,7 @@ export const useBasicAuthSecretDropdown = ({ }); } }, - [ - supportNone, - isCreateSecretModalOpen, - launchModal, - namespace, - currentSecretName, - setFieldValue, - ], + [isCreateSecretModalOpen, launchModal, namespace, currentSecretName, setFieldValue], ); return { handleSecretChange }; From 453eaf586fde70018464ca11f83892acc1ae9920 Mon Sep 17 00:00:00 2001 From: sowmya-sl Date: Fri, 24 Jul 2026 21:56:01 +0530 Subject: [PATCH 7/7] HELM-763: Fix secret dropdown search and harden action keys - Fix autocompleteFilter crash when fuzzysearch receives undefined haystack for non-React-element items; fall back to string value or key - Rename CREATE_SECRET_KEY from 'create-secret' to '__create_secret__' to prevent collision with a real Kubernetes secret of that name - Align HelmURLInstallForm help text to use interpolated i18n variant and remove orphaned locale key Co-authored-by: Cursor --- frontend/packages/helm-plugin/locales/en/helm-plugin.json | 1 - .../forms/install-upgrade/HelmInstallUpgradeForm.tsx | 4 ++-- .../src/components/forms/url-chart/HelmURLChartForm.tsx | 4 ++-- .../src/components/forms/url-chart/HelmURLInstallForm.tsx | 3 ++- .../components/forms/url-chart/useBasicAuthSecretDropdown.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/packages/helm-plugin/locales/en/helm-plugin.json b/frontend/packages/helm-plugin/locales/en/helm-plugin.json index 5e8904b166d..7b35ec9c304 100644 --- a/frontend/packages/helm-plugin/locales/en/helm-plugin.json +++ b/frontend/packages/helm-plugin/locales/en/helm-plugin.json @@ -6,7 +6,6 @@ "**Helm Charts** are packages for deploying an Application or components of a larger Application.": "**Helm Charts** are packages for deploying an Application or components of a larger Application.", "A description for the Helm Chart repository.": "A description for the Helm Chart repository.", "A display name for the Helm Chart repository.": "A display name for the Helm Chart repository.", - "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication": "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication", "A unique name for the Helm Chart repository.": "A unique name for the Helm Chart repository.", "A unique name for the Helm Release.": "A unique name for the Helm Release.", "Add a Helm Chart Repository to extend the Developer Catalog": "Add a Helm Chart Repository to extend the Developer Catalog", diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx index 0a635e2e1c8..58056d0af78 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradeForm.tsx @@ -91,8 +91,8 @@ const HelmInstallUpgradeForm: FC< const helmReadmeModalLauncher = useHelmReadmeModalLauncher({ readme: chartReadme }); const showAuthSecret = values.isURLInstall; const secretResources = useSecretResources(namespace); - const autocompleteFilter = (strText: string, item: any): boolean => - fuzzy(strText, item?.props?.name); + const autocompleteFilter = (strText: string, item: any, key?: string): boolean => + fuzzy(strText, item?.props?.name || (typeof item === 'string' ? item : key) || ''); const secretMissing = useMemo(() => { if ( !showAuthSecret || diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx index 657dad4a83c..d336e975905 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx @@ -39,8 +39,8 @@ const HelmURLChartForm: FC & HelmURLChartFormP setFieldValue, }); - const autocompleteFilter = (strText: string, item: any): boolean => - fuzzy(strText, item?.props?.name); + const autocompleteFilter = (strText: string, item: any, key?: string): boolean => + fuzzy(strText, item?.props?.name || (typeof item === 'string' ? item : key) || ''); const secretResources = useSecretResources(namespace); const isNextDisabled = !isValid || !dirty || isSubmitting; diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLInstallForm.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLInstallForm.tsx index 9910c4f8306..90c2044bd11 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLInstallForm.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLInstallForm.tsx @@ -158,7 +158,8 @@ const HelmURLInstallForm: FC & HelmURLInstal autocompleteFilter={autocompleteFilter} disabled helpText={t( - 'A secret with "username" and "password" keys for OCI/HTTP(S) authentication', + 'Secret with "{{username}}" and "{{password}}" keys for OCI/HTTP(S) authentication.', + { username: 'username', password: 'password' }, )} /> diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts b/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts index 782e7f388d9..34825846500 100644 --- a/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts +++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/useBasicAuthSecretDropdown.ts @@ -1,7 +1,7 @@ import { useState, useCallback } from 'react'; import { useHelmCreateBasicAuthSecretModal } from './HelmCreateBasicAuthSecretModal'; -const CREATE_SECRET_KEY = 'create-secret'; +const CREATE_SECRET_KEY = '__create_secret__'; const NONE_SECRET_KEY = '__none__'; export { CREATE_SECRET_KEY, NONE_SECRET_KEY };