Skip to content
Open
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.",
"Select a secret": "Select a secret",
"A secret with \"username\" and \"password\" keys for OCI/HTTP(S) authentication": "A secret with \"username\" and \"password\" keys for OCI/HTTP(S) 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",
"Secret for basic authentication": "Secret for basic authentication",
"Install": "Install",
"Back": "Back",
"Display Name": "Display Name",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import type { FC } from 'react';
import { useEffect } from 'react';
import { useEffect, useMemo } 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 { InputField, FormFooter, FormBody, FormHeader, FlexForm } from '@console/shared';
import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook';
import { SecretModel } from '@console/internal/models';
import type { K8sResourceKind } from '@console/internal/module/k8s';
import {
InputField,
FormFooter,
FormBody,
FormHeader,
FlexForm,
ResourceDropdownField,
} from '@console/shared';
import type { HelmURLChartFormData } from './types';

export interface HelmURLChartFormProps {
Expand All @@ -17,6 +28,7 @@ const HelmURLChartForm: FC<FormikProps<HelmURLChartFormData> & HelmURLChartFormP
status,
isSubmitting,
onNext,
namespace,
isValid,
dirty,
values,
Expand All @@ -25,6 +37,34 @@ const HelmURLChartForm: FC<FormikProps<HelmURLChartFormData> & HelmURLChartFormP
}) => {
const { t } = useTranslation();

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

const watchedResources = useK8sWatchResources<{
secrets: K8sResourceKind[];
}>({
secrets: {
isList: true,
kind: SecretModel.kind,
namespace,
optional: true,
},
});
const secretResources = 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,
],
);
const isNextDisabled = !isValid || !dirty || isSubmitting;

// Auto-populate releaseName and chartVersion from URL
Expand Down Expand Up @@ -116,6 +156,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.')}
Comment on lines +161 to +162
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.

The label at HelmURLInstallForm.tsx line 185 doesn't have a trailing period...should we remove the one here?

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,48 @@ 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);
let authParam = '';
if (basicAuthSecretName) {
authParam = `&basic_auth_secret_name=${encodeURIComponent(basicAuthSecretName)}`;
}
Comment on lines +78 to +81
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.

Alternatively, I believe you can use a ternary,

Suggested change
let authParam = '';
if (basicAuthSecretName) {
authParam = `&basic_auth_secret_name=${encodeURIComponent(basicAuthSecretName)}`;
}
const authParam = basicAuthSecretName ? `&basic_auth_secret_name=${encodeURIComponent(basicAuthSecretName)}` : '';

which avoids the need for using let.

const apiUrl = `/api/helm/chart?url=${encodeURIComponent(
fullChartURL,
)}&noRepo=true&namespace=${namespace}${authParam}`;
Comment on lines +82 to +84
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.

This might be prettier if you went ahead and used one more local variable:

Suggested change
const apiUrl = `/api/helm/chart?url=${encodeURIComponent(
fullChartURL,
)}&noRepo=true&namespace=${namespace}${authParam}`;
const urlParam = encodeURIComponent(fullChartURL)
const apiUrl = `/api/helm/chart?url=${urlParam}&noRepo=true&namespace=${namespace}${authParam}`;


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 +122,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 +171,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 +216,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?

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ 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';
import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook';
import { SecretModel } from '@console/internal/models';
import type { K8sResourceKind } from '@console/internal/module/k8s';
import {
InputField,
ResourceDropdownField,
FormFooter,
FormBody,
CodeEditorField,
Expand Down Expand Up @@ -36,12 +41,44 @@ 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 watchedResources = useK8sWatchResources<{
secrets: K8sResourceKind[];
}>({
secrets: {
isList: true,
kind: SecretModel.kind,
namespace,
optional: true,
},
});

const secretResources = 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,
],
);

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

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

Expand Down Expand Up @@ -142,6 +179,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~Select a secret')}
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
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,
});
};
14 changes: 13 additions & 1 deletion pkg/helm/actions/get_chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,25 @@ 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 err := applyBasicAuthFromSecret(cmd, coreClient, namespace, basicAuthSecretName); err != nil {
return nil, err
}
if basicAuthSecretName != "" {
rc, err := RegistryClientWithBasicAuth(false, false, cmd.Username, cmd.Password)
Comment on lines +75 to +79
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.

This structure strikes me as odd: I suspect that we would prefer not to call applyBasicAuthFromSecret() when there is no secret, and we're gonna check for the secret right after the call...so, why not do the check first and call it only if there is a secret?

Suggested change
if err := applyBasicAuthFromSecret(cmd, coreClient, namespace, basicAuthSecretName); err != nil {
return nil, err
}
if basicAuthSecretName != "" {
rc, err := RegistryClientWithBasicAuth(false, false, cmd.Username, cmd.Password)
if basicAuthSecretName != "" {
if err := applyBasicAuthFromSecret(cmd, coreClient, namespace, basicAuthSecretName); err != nil {
return nil, err
}
rc, err := RegistryClientWithBasicAuth(false, false, cmd.Username, cmd.Password)

(You might need to tweak the declaration/usage of err since it gets reused.)

if err != nil {
return nil, fmt.Errorf("failed to configure OCI registry client: %w", err)
}
cmd.SetRegistryClient(rc)
}
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
33 changes: 24 additions & 9 deletions pkg/helm/actions/get_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,8 @@ import (
// 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)
}

func GetOCIRegistry(conf *action.Configuration, skipTLSVerify bool, plainHTTP bool) error {
if conf == nil {
return fmt.Errorf("action configuration cannot be nil")
}
// registryClientOptions returns the same options used by GetOCIRegistry for TLS / plain-HTTP behavior.
func registryClientOptions(skipTLSVerify, plainHTTP bool) []registry.ClientOption {
Comment on lines +15 to +16
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.

This docstring isn't very useful, and, worse, it is describing the behavior of code which is elsewhere (which risks having that code change without this description being updated).

Can you rework this in a way which doesn't mention GetOCIRegistry()? (I would suggest stealing GetOCIRegistry()'s docstring, but it doesn't have one! 😛)

(FWIW, the fact that the guts of this function used to be the guts of GetOCIRegistry() won't be interesting going forward.)

opts := []registry.ClientOption{
registry.ClientOptDebug(false),
}
Expand All @@ -33,7 +27,28 @@ func GetOCIRegistry(conf *action.Configuration, skipTLSVerify bool, plainHTTP bo
}
opts = append(opts, registry.ClientOptHTTPClient(&http.Client{Transport: transport}))
}
registryClient, err := newRegistryClient(opts...)
return opts
}

// RegistryClientWithBasicAuth builds a registry.Client with the same TLS/plain-HTTP settings as
// GetDefaultOCIRegistry (skipTLSVerify=false, plainHTTP=false) plus OCI basic auth.
// Helm's OCI getter uses Configuration.RegistryClient when set and does not apply ChartPathOptions
// username/password to that client; credentials must be set on the registry client via ClientOptBasicAuth.
func RegistryClientWithBasicAuth(skipTLSVerify, plainHTTP bool, username, password string) (*registry.Client, error) {
opts := registryClientOptions(skipTLSVerify, plainHTTP)
opts = append(opts, registry.ClientOptBasicAuth(username, password))
return newRegistryClient(opts...)
}

func GetDefaultOCIRegistry(conf *action.Configuration) error {
return GetOCIRegistry(conf, false, false)
}

func GetOCIRegistry(conf *action.Configuration, skipTLSVerify bool, plainHTTP bool) error {
if conf == nil {
return fmt.Errorf("action configuration cannot be nil")
}
registryClient, err := newRegistryClient(registryClientOptions(skipTLSVerify, plainHTTP)...)
if err != nil {
return fmt.Errorf("failed to create registry client: %w", err)
}
Expand Down
Loading