Skip to content
Draft
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",
"None": "None",
"Install": "Install",
"Back": "Back",
"Display Name": "Display Name",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ 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 { InputField, FormFooter, FormBody, FormHeader, FlexForm } from '@console/shared';
import {
InputField,
FormFooter,
FormBody,
FormHeader,
FlexForm,
ResourceDropdownField,
} from '@console/shared';
import type { HelmURLChartFormData } from './types';
import { useSecretResources } from './useSecretResources';

export interface HelmURLChartFormProps {
namespace: string;
Expand All @@ -17,6 +26,7 @@ const HelmURLChartForm: FC<FormikProps<HelmURLChartFormData> & HelmURLChartFormP
status,
isSubmitting,
onNext,
namespace,
isValid,
dirty,
values,
Expand All @@ -25,6 +35,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 @@ -116,6 +130,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}`;

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
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ 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 {
InputField,
ResourceDropdownField,
FormFooter,
FormBody,
CodeEditorField,
Expand All @@ -18,6 +20,7 @@ import {
import { getJSONSchemaOrder, prune } from '@console/shared/src/components/dynamic-form/utils';
import { useHelmReadmeModalLauncher } from '../install-upgrade/HelmReadmeModal';
import type { HelmURLInstallFormData } from './types';
import { useSecretResources } from './useSecretResources';

export interface HelmURLInstallFormProps {
chartHasValues: boolean;
Expand All @@ -36,12 +39,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 @@ -142,6 +152,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.ChartPathOptions, cmd, userCredentials); err != nil {
return nil, err
}
}
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

}

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