Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frontend/packages/helm-plugin/locales/en/helm-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,16 @@
"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",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does "secret" here refer to a Kubernetes Secret resource? if so, should it be capitalized "Secret" to match how OpenShift usually refers to that object type?

also, "basic" in "basic authentication" always gets a cap B.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes secret is for K8 secret so it is capitalized. "Basic" is fixed in #16490

"Select a secret": "Select a secret",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any helper text, tooltip, link for this "Select a secret" field to tells users how to create one if they don't have one?

also, Secret cap S if you mean the Kube kind (global)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a text under the dropdown which says that the Secret should have the keys username and password.

"A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication": "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be listed separately for clarity, for ex., "A Secret with username and password keys for authenticating with OCI or HTTP/HTTPS Helm chart URLs"?

And should username and password be formatted as code/monospace since they're literal key names the user must match exactly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be listed separately for clarity, for ex., "A Secret with username and password keys for authenticating with OCI or HTTP/HTTPS Helm chart URLs"?

This text is to appear under the dropdown to select Secrets. I thought OCI, HTTP(S) charts was understood, since the chart URL section will mention OCI or HTTP(S).

And should username and password be formatted as code/monospace since they're literal key names the user must match exactly?

Yes, username and password are the key names for Secret. Formatted as monospace is possible ? Can you share any examples in console for me reference?

"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",
"None": "None",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does "None" display? default drop-down option for Version field? If so, would "Latest" or "No specific version" be clearer to users about what will happen if they don't select it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None displays in the Secret field if the user has not selected any Secrets.

"Install": "Install",
"Back": "Back",
"Display Name": "Display Name",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import type { FC } from 'react';
import { useEffect } from 'react';
import { TextInputTypes, Grid, GridItem } from '@patternfly/react-core';
import type { FormikProps } from 'formik';
import * as fuzzy from 'fuzzysearch';
import { useTranslation } from 'react-i18next';
import FormSection from '@console/dev-console/src/components/import/section/FormSection';
import { FlexForm } from '@console/shared/src/components/form-utils/FlexForm';
import { FormBody } from '@console/shared/src/components/form-utils/FormBody';
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 type { HelmURLChartFormData } from './types';
import { useSecretResources } from './useSecretResources';

export interface HelmURLChartFormProps {
namespace: string;
Expand All @@ -21,6 +24,7 @@ const HelmURLChartForm: FC<FormikProps<HelmURLChartFormData> & HelmURLChartFormP
status,
isSubmitting,
onNext,
namespace,
isValid,
dirty,
values,
Expand All @@ -29,6 +33,10 @@ const HelmURLChartForm: FC<FormikProps<HelmURLChartFormData> & HelmURLChartFormP
}) => {
const { t } = useTranslation();

const autocompleteFilter = (strText: string, item: any): boolean =>
fuzzy(strText, item?.props?.name);

const secretResources = useSecretResources(namespace);
const isNextDisabled = !isValid || !dirty || isSubmitting;

// Auto-populate releaseName and chartVersion from URL
Expand Down Expand Up @@ -120,6 +128,21 @@ const HelmURLChartForm: FC<FormikProps<HelmURLChartFormData> & HelmURLChartFormP
data-test="oci-chart-version"
/>
</GridItem>
<GridItem md={12}>
<ResourceDropdownField
name="basicAuthSecretName"
label={t('helm-plugin~Secret for basic authentication')}
resources={secretResources}
dataSelector={['metadata', 'name']}
fullWidth
placeholder={t('helm-plugin~Select a secret')}
showBadge
autocompleteFilter={autocompleteFilter}
helpText={t(
'helm-plugin~A secret with "username" and "password" keys for OCI/HTTP(S) authentication',
)}
/>
</GridItem>
</Grid>
</FormSection>
</FormBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,38 +65,47 @@ const HelmURLChartInstallPage: FunctionComponent = () => {
chartURL: '',
chartVersion: '',
namespace,
basicAuthSecretName: '',
};

const fetchChartData = useCallback(async (chartURL: string, chartVersion: string) => {
setIsLoadingChart(true);
setChartError(null);
const fetchChartData = useCallback(
async (chartURL: string, chartVersion: string, basicAuthSecretName: string) => {
setIsLoadingChart(true);
setChartError(null);

try {
const fullChartURL = getFullChartURL(chartURL, chartVersion);
const apiUrl = `/api/helm/chart?url=${encodeURIComponent(fullChartURL)}&noRepo=true`;
try {
const fullChartURL = getFullChartURL(chartURL, chartVersion);
const authParam = basicAuthSecretName
? `&basic_auth_secret_name=${encodeURIComponent(basicAuthSecretName)}`
: '';
const apiUrl = `/api/helm/chart?url=${encodeURIComponent(
fullChartURL,
)}&noRepo=true&namespace=${namespace}${authParam}`;
Comment thread
sowmya-sl marked this conversation as resolved.

const res = await coFetchJSON(apiUrl);
const chart: HelmChart = res?.chart || res;
const valuesYAML = getChartValuesYAML(chart);
const valuesJSON = chart?.values ?? {};
const valuesSchema = chart?.schema && JSON.parse(atob(chart?.schema));
const res = await coFetchJSON(apiUrl);
const chart: HelmChart = res?.chart || res;
const valuesYAML = getChartValuesYAML(chart);
const valuesJSON = chart?.values ?? {};
const valuesSchema = chart?.schema && JSON.parse(atob(chart?.schema));

setInitialYamlData(valuesYAML);
setInitialFormData(valuesJSON as Record<string, unknown>);
setInitialFormSchema(valuesSchema);
setChartHasValues(!!valuesYAML);
setChartData(chart);
} catch (e) {
setChartError(e as Error);
} finally {
setIsLoadingChart(false);
}
}, []);
setInitialYamlData(valuesYAML);
setInitialFormData(valuesJSON as Record<string, unknown>);
setInitialFormSchema(valuesSchema);
setChartHasValues(!!valuesYAML);
setChartData(chart);
} catch (e) {
setChartError(e as Error);
} finally {
setIsLoadingChart(false);
}
},
[namespace],
);

const handleNextStep = useCallback(
(values: HelmURLChartFormData) => {
setChartDetails(values);
fetchChartData(values.chartURL, values.chartVersion);
fetchChartData(values.chartURL, values.chartVersion, values.basicAuthSecretName);
setCurrentStep(WizardStep.ConfigureInstall);
},
[fetchChartData],
Expand All @@ -112,7 +121,15 @@ const HelmURLChartInstallPage: FunctionComponent = () => {
values: HelmURLInstallFormData,
actions: FormikHelpers<HelmURLInstallFormData>,
) => {
const { releaseName, chartURL, chartVersion, yamlData, formData, editorType } = values;
const {
releaseName,
chartURL,
chartVersion,
yamlData,
formData,
editorType,
basicAuthSecretName,
} = values;

let valuesObj: Record<string, unknown> | undefined;
if (editorType === EditorType.Form) {
Expand Down Expand Up @@ -153,6 +170,7 @@ const HelmURLChartInstallPage: FunctionComponent = () => {
chart_url: fullChartURL, // eslint-disable-line @typescript-eslint/naming-convention
...(chartVersion ? { chart_version: chartVersion } : {}), // eslint-disable-line @typescript-eslint/naming-convention
...(valuesObj ? { values: valuesObj } : {}),
...(basicAuthSecretName ? { basic_auth_secret_name: basicAuthSecretName } : {}), // eslint-disable-line @typescript-eslint/naming-convention
noRepo: true,
};

Expand Down Expand Up @@ -197,6 +215,7 @@ const HelmURLChartInstallPage: FunctionComponent = () => {
chartURL: chartDetails?.chartURL || '',
chartVersion: chartDetails?.chartVersion || '',
namespace,
basicAuthSecretName: chartDetails?.basicAuthSecretName || '',
chartName: chartData?.metadata?.name || '',
appVersion: chartData?.metadata?.appVersion || '',
chartReadme: getChartReadme(chartData),
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes seem very similar to the ones in HelmURLChartForm.tsx, would it be appropriate to refactor these into common code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which function are you referring to here?

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactNode, FC } 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';
import * as _ from 'lodash';
import { Trans, useTranslation } from 'react-i18next';
import FormSection from '@console/dev-console/src/components/import/section/FormSection';
Expand All @@ -13,9 +14,11 @@ 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 { useHelmReadmeModalLauncher } from '../install-upgrade/HelmReadmeModal';
import type { HelmURLInstallFormData } from './types';
import { useSecretResources } from './useSecretResources';

export interface HelmURLInstallFormProps {
chartHasValues: boolean;
Expand All @@ -34,12 +37,19 @@ const HelmURLInstallForm: FC<FormikProps<HelmURLInstallFormData> & HelmURLInstal
values,
chartMetaDescription,
chartError,
namespace,
onBack,
}) => {
const { t } = useTranslation();
const { chartReadme, formData, formSchema } = values;

const helmReadmeModalLauncher = useHelmReadmeModalLauncher({ readme: chartReadme });
const autocompleteFilter = (strText: string, item: string): boolean => fuzzy(strText, item);

const secretResources = useSecretResources(namespace);

const helmReadmeModalLauncher = useHelmReadmeModalLauncher({
readme: chartReadme,
});

const isSubmitDisabled = isSubmitting || !_.isEmpty(errors) || !!chartError;

Expand Down Expand Up @@ -140,6 +150,22 @@ const HelmURLInstallForm: FC<FormikProps<HelmURLInstallFormData> & HelmURLInstal
data-test="chart-version"
/>
</GridItem>
<GridItem xl={3} lg={3} md={12}>
<ResourceDropdownField
name="basicAuthSecretName"
label={t('helm-plugin~Secret for basic authentication')}
resources={secretResources}
dataSelector={['metadata', 'name']}
fullWidth
placeholder={t('helm-plugin~None')}
showBadge
autocompleteFilter={autocompleteFilter}
disabled
helpText={t(
'helm-plugin~A secret with "username" and "password" keys for OCI/HTTP(S) authentication',
)}
/>
</GridItem>
</Grid>
</FormSection>
{!chartError &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface HelmURLChartFormData {
chartURL: string;
chartVersion: string;
namespace: string;
basicAuthSecretName?: string;
}

export interface HelmURLInstallFormData extends HelmURLChartFormData {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useMemo } from 'react';
import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook';
import { SecretModel } from '@console/internal/models';
import type { K8sResourceKind } from '@console/internal/module/k8s';

export const useSecretResources = (namespace: string) => {
const watchedResources = useK8sWatchResources<{
secrets: K8sResourceKind[];
}>({
secrets: {
isList: true,
kind: SecretModel.kind,
namespace,
optional: true,
},
});

return useMemo(
() => [
{
data: watchedResources.secrets?.data,
loaded: watchedResources.secrets?.loaded,
loadError: watchedResources.secrets?.loadError,
kind: SecretModel.kind,
},
],
[
watchedResources.secrets?.data,
watchedResources.secrets?.loaded,
watchedResources.secrets?.loadError,
],
);
};
2 changes: 2 additions & 0 deletions frontend/packages/helm-plugin/src/utils/helm-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,15 @@ export const installChartFromURL = (
chartURL: string,
chartVersion?: string,
values?: Record<string, unknown>,
basicAuthSecretName?: string,
) => {
return coFetchJSON.post('/api/helm/release/async', {
namespace,
name: releaseName,
chart_url: chartURL, // eslint-disable-line @typescript-eslint/naming-convention
...(chartVersion ? { chart_version: chartVersion } : {}), // eslint-disable-line @typescript-eslint/naming-convention
...(values ? { values } : {}),
...(basicAuthSecretName ? { basic_auth_secret_name: basicAuthSecretName } : {}), // eslint-disable-line @typescript-eslint/naming-convention
noRepo: true,
});
};
3 changes: 2 additions & 1 deletion pkg/helm/actions/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ func GetActionConfigurations(host, ns, token string, transport *http.RoundTrippe
}
conf := new(action.Configuration)
conf.Init(confFlags, ns, "secrets", klog.Infof)
err = GetDefaultOCIRegistry(conf)
registryClient, err := GetDefaultOCIRegistry()
if err != nil {
klog.V(4).Infof("Failed to get default OCI registry: %v", err)
}
conf.RegistryClient = registryClient
return conf
}
13 changes: 12 additions & 1 deletion pkg/helm/actions/get_chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,24 @@ func GetChart(url string, conf *action.Configuration, repositoryNamespace string
return loader.Load(chartPath)
}

func GetChartFromURL(url string, conf *action.Configuration, namespace string, client dynamic.Interface, coreClient corev1client.CoreV1Interface, filesCleanup bool) (*chart.Chart, error) {
// GetChartFromURL loads a chart from an OCI or direct HTTP(S) URL. basicAuthSecretName names a
// Secret in namespace with username and password keys when the registry requires authentication.
func GetChartFromURL(url string, conf *action.Configuration, namespace string, client dynamic.Interface, coreClient corev1client.CoreV1Interface, filesCleanup bool, basicAuthSecretName string) (*chart.Chart, error) {

if !isValidChartURL(url) {
return nil, fmt.Errorf("invalid chart URL: %s, must be oci:// URL or http(s)://*.tgz", url)
}
cmd := action.NewInstall(conf)
cmd.Namespace = namespace
if basicAuthSecretName != "" {
userCredentials, err := GetUserCredentials(coreClient, namespace, basicAuthSecretName)
if err != nil {
return nil, err
}
if err := applyBasicAuthFromUserCredentials(cmd, userCredentials); err != nil {
return nil, err
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
chartLocation, err := cmd.ChartPathOptions.LocateChart(url, settings)
if err != nil {
return nil, fmt.Errorf("error getting chart from URL: %v", err)
Expand Down
24 changes: 14 additions & 10 deletions pkg/helm/actions/get_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,18 @@ import (
"fmt"
"net/http"

"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/registry"
)

// newRegistryClient is a package-level variable to allow mocking in tests
var newRegistryClient = registry.NewClient

func GetDefaultOCIRegistry(conf *action.Configuration) error {
return GetOCIRegistry(conf, false, false)
type UserCredentials struct {
Username string
Password string
}

func GetOCIRegistry(conf *action.Configuration, skipTLSVerify bool, plainHTTP bool) error {
if conf == nil {
return fmt.Errorf("action configuration cannot be nil")
}
func GetOCIRegistry(skipTLSVerify bool, plainHTTP bool, userCredentials *UserCredentials) (*registry.Client, error) {
opts := []registry.ClientOption{
registry.ClientOptDebug(false),
}
Expand All @@ -33,10 +30,17 @@ func GetOCIRegistry(conf *action.Configuration, skipTLSVerify bool, plainHTTP bo
}
opts = append(opts, registry.ClientOptHTTPClient(&http.Client{Transport: transport}))
}
if userCredentials != nil {
opts = append(opts, registry.ClientOptBasicAuth(userCredentials.Username, userCredentials.Password))
}
registryClient, err := newRegistryClient(opts...)
if err != nil {
return fmt.Errorf("failed to create registry client: %w", err)
return nil, fmt.Errorf("failed to create registry client: %w", err)
}
conf.RegistryClient = registryClient
return nil
return registryClient, nil
Comment on lines +36 to +40
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this error really providing value? Otherwise, I think you can simplify this to just

	return newRegistryClient(opts...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to have some error logging for creation of registry client. I will be needed when creating a registry Client object with authentication or any other future modifications fails.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree, but this code isn't performing any logging -- it's just adding text to the error message. Following the call-chain back up, eventually we hit a caller which does do error logging, and it reports "Failed to get default OCI registry"...so (I think...) we already have the context of creating a registry, so the text that we're adding here doesn't actually provide any additional information.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is also getting called in install_chart.go and get_chart.go. That surfaces the error if this fails.


}

func GetDefaultOCIRegistry() (*registry.Client, error) {
return GetOCIRegistry(false, false, nil)
}
Loading