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
11 changes: 11 additions & 0 deletions frontend/packages/helm-plugin/console-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@
"required": ["OPENSHIFT_HELM"]
}
},
{
"type": "console.page/route",
"properties": {
"exact": true,
"path": ["/helm/ns/:ns/url-chart"],
"component": { "$codeRef": "HelmURLChartInstallPage" }
},
"flags": {
"required": ["OPENSHIFT_HELM"]
}
},
{
"type": "dev-console.add/action-group",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@helm
Feature: Install Helm Chart from URL
As a user, I want to install a Helm chart from an OCI or HTTP URL


Background:
Given user has created or selected namespace "aut-helm-url"


@smoke
Scenario: Navigate to URL chart install page from Helm tab: HR-URL-TC01
Given user is at the Helm page
When user clicks on Create menu and selects "Install a Helm Chart from a URL"
Then user is redirected to the URL chart install page


@smoke
Scenario: Validate required fields on URL chart form: HR-URL-TC02
Given user is at the URL chart install page
When user clicks on the Next button without filling any fields
Then user will see validation errors for Chart URL, Release name, and Chart version


@regression
Scenario: Validate invalid chart URL format: HR-URL-TC03
Given user is at the URL chart install page
When user enters "not-a-valid-url" as Chart URL
And user enters Release Name as "test-release"
And user enters Chart Version as "1.0.0"
And user clicks on the Next button
Then user will see a validation error for invalid Chart URL format


@smoke
Scenario: Install Helm Chart from HTTP URL: HR-URL-TC04
Given user is at the URL chart install page
When user enters "https://redhat-developer.github.io/redhat-helm-charts/charts/dotnet-0.0.1.tgz" as Chart URL
And user enters Release Name as "dotnet-url-test"
And user enters Chart Version as "0.0.1"
And user clicks on the Next button
And user clicks on the Install button
Then user will be redirected to Topology page


@smoke
Scenario: Install Helm Chart from OCI registry: HR-URL-TC05
Given user is at the URL chart install page
When user enters "oci://ghcr.io/stefanprodan/charts/podinfo" as Chart URL
And user enters Release Name as "podinfo-oci-test"
And user enters Chart Version as "6.7.1"
And user clicks on the Next button
And user clicks on the Install button
Then user will be redirected to Topology page


@regression
Scenario: Upgrade a URL-installed Helm release: HR-URL-TC06
Given user is on the Helm page with helm release "dotnet-url-test"
When user clicks on the Kebab menu
And user clicks on the "Upgrade" action
And user clicks on the Install button
Then user will be redirected to Topology page
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './helm-details-page';
export * from './helm-page';
export * from './rollBack-helm-release-page';
export * from './upgrade-helm-release-page';
export * from './url-chart-install-page';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const urlChartPO = {
chartURL: '[data-test="oci-chart-url"] input',
releaseName: '[data-test="oci-release-name"] input',
chartVersion: '[data-test="oci-chart-version"] input',
nextButton: '[data-test-id="submit-button"]',
cancelButton: '[data-test-id="reset-button"]',
installButton: '[data-test-id="submit-button"]',
backButton: '[data-test-id="reset-button"]',
};

export const urlChartInstallPage = {
enterChartURL: (url: string) => {
cy.get(urlChartPO.chartURL).clear().type(url);
},
enterReleaseName: (name: string) => {
cy.get(urlChartPO.releaseName).clear().type(name);
},
enterChartVersion: (version: string) => {
cy.get(urlChartPO.chartVersion).clear().type(version);
},
clickNext: () => {
cy.get(urlChartPO.nextButton).click();
},
clickInstall: () => {
cy.get(urlChartPO.installButton).click();
},
verifyValidationErrors: () => {
cy.get('.pf-m-error').should('have.length.at.least', 1);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants';
import { navigateTo } from '@console/dev-console/integration-tests/support/pages';
import { topologyPage } from '@console/topology/integration-tests/support/pages/topology/topology-page';
import { urlChartInstallPage } from '../../pages';

Given('user is at the URL chart install page', () => {
navigateTo(devNavigationMenu.Helm);
cy.byLegacyTestID('item-create').click();
cy.get('[data-test-dropdown-menu]').contains('Helm Chart URL').click();
});

When('user clicks on Create menu and selects "Install a Helm Chart from a URL"', () => {
cy.byLegacyTestID('item-create').click();
cy.get('[data-test-dropdown-menu]').contains('Helm Chart URL').click();
});

Then('user is redirected to the URL chart install page', () => {
cy.url().should('include', '/url-chart');
cy.get('[data-test="oci-chart-url"]').should('be.visible');
});

When('user clicks on the Next button without filling any fields', () => {
urlChartInstallPage.clickNext();
});

Then('user will see validation errors for Chart URL, Release name, and Chart version', () => {
urlChartInstallPage.verifyValidationErrors();
});

When('user enters {string} as Chart URL', (url: string) => {
urlChartInstallPage.enterChartURL(url);
});

When('user enters Release Name as {string}', (name: string) => {
urlChartInstallPage.enterReleaseName(name);
});

When('user enters Chart Version as {string}', (version: string) => {
urlChartInstallPage.enterChartVersion(version);
});

When('user clicks on the Next button', () => {
urlChartInstallPage.clickNext();
});

When('user clicks on the Install button', () => {
urlChartInstallPage.clickInstall();
});

Then('user will see a validation error for invalid Chart URL format', () => {
cy.get('.pf-m-error').should('exist');
});

Then('user will be redirected to Topology page', () => {
topologyPage.verifyTopologyPage();
});
17 changes: 17 additions & 0 deletions frontend/packages/helm-plugin/locales/en/helm-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@
"Invalid YAML - {{errorText}}": "Invalid YAML - {{errorText}}",
"Select the version to rollback <1>{{releaseName}}</1> to, from the table below:": "Select the version to rollback <1>{{releaseName}}</1> to, from the table below:",
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 be "roll back" bc it's a verb

also, we should avoid using directional words (like "below") in case something moves. if this text is placed such that a user will know where to make the selection w/o more detail, cut this to "Select the version to roll back to."

if a user might need detail, name the table instead of saying where it is: "From the {Version menu}, select the version to roll back to." (or "Select the...from the {menu name}." is fine too. i just prefer orienting a user before giving them an instruction. your call!)

Copy link
Copy Markdown
Contributor Author

@sowmya-sl sowmya-sl Mar 4, 2026

Choose a reason for hiding this comment

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

Ack. These are existing changes. I will follow up to address it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know. Sorry! If I see it I feel remiss not to comment.

"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.",
"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",
"Install": "Install",
"Back": "Back",
"Display Name": "Display Name",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--> name

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.

Ack. Its an existing change, I would like to take it up separately if that is okay.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yes :)

"Namespace": "Namespace",
"Disabled": "Disabled",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

but if it's not even displayed, change to Unavailable

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.

Ack and same as above.

Expand All @@ -125,6 +141,7 @@
"Filter by status": "Filter by status",
"No Helm Releases found": "No Helm Releases found",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--> releases

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.

Ack and same as above.

"Browse the catalog to discover available Helm Charts": "Browse the catalog to discover available Helm Charts",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--> charts

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.

Same as above

"Helm chart URL": "Helm chart URL",
"Repositories": "Repositories",
"Select a Project to view its details<1></1>.": "Select a Project to view its details<1></1>.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

project

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.

Ack. Its an existing change, I would like to take it up separately if that is okay.

"Helm Repositories": "Helm Repositories",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

repositories

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.

Same as above.

Expand Down
3 changes: 2 additions & 1 deletion frontend/packages/helm-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"helmTopologySidebarTabSections": "src/topology/sidebar/release-panel/tab-sections.tsx",
"helmIcons": "src/utils/icons.tsx",
"helmDetectionProvider": "src/providers/helm-detection-provider.ts",
"useHelmChartRepositoriesBreadcrumbs": "src/providers/useHelmChartRepositoriesBreadcrumbs.ts"
"useHelmChartRepositoriesBreadcrumbs": "src/providers/useHelmChartRepositoriesBreadcrumbs.ts",
"HelmURLChartInstallPage": "src/components/forms/url-chart/HelmURLChartInstallPage.tsx"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type { FC } from 'react';
import { useEffect } from 'react';
import { TextInputTypes, Grid, GridItem } from '@patternfly/react-core';
import type { FormikProps } from 'formik';
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 type { HelmURLChartFormData } from './types';

export interface HelmURLChartFormProps {
namespace: string;
onNext: () => void;
}

const HelmURLChartForm: FC<FormikProps<HelmURLChartFormData> & HelmURLChartFormProps> = ({
handleReset,
status,
isSubmitting,
onNext,
isValid,
dirty,
values,
setFieldValue,
setFieldError,
}) => {
const { t } = useTranslation();

const isNextDisabled = !isValid || !dirty || isSubmitting;

// Auto-populate releaseName and chartVersion from URL
useEffect(() => {
if (!values.chartURL) return;
let url: URL;
try {
url = new URL(values.chartURL);
} catch {
setFieldError('chartURL', t('helm-plugin~Invalid chart URL format.'));
return;
}
const scheme = url.protocol;
const filename = url.pathname.split('/').pop() || '';
Comment on lines +31 to +41
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx | sed -n '25,40p'

Repository: openshift/console

Length of output: 683


🏁 Script executed:

rg "URL\.parse" frontend --type=ts --type=tsx -C2

Repository: openshift/console

Length of output: 88


🏁 Script executed:

rg "import.*URL" frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx

Repository: openshift/console

Length of output: 108


🏁 Script executed:

rg "URL\.parse" frontend -C2

Repository: openshift/console

Length of output: 623


🏁 Script executed:

rg "URL\.parse|new URL" frontend/packages/helm-plugin --type ts -C2

Repository: openshift/console

Length of output: 2047


🏁 Script executed:

fd -e ts -e tsx frontend/packages/helm-plugin/src | xargs grep -l "URL" | head -10

Repository: openshift/console

Length of output: 498


Replace URL.parse with the standard URL constructor.

URL.parse isn't part of the browser URL API (it's a Node.js v18+ method), so this effect will throw at runtime and break the auto-population of releaseName and chartVersion. Use new URL() with try/catch to handle invalid URLs gracefully.

Proposed fix
-    const url = URL.parse(values.chartURL);
-    if (!url) return;
+    let url: URL;
+    try {
+      url = new URL(values.chartURL);
+    } catch {
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!values.chartURL) return;
const url = URL.parse(values.chartURL);
if (!url) return;
const scheme = url.protocol;
const filename = url.pathname.split('/').pop() || '';
useEffect(() => {
if (!values.chartURL) return;
let url: URL;
try {
url = new URL(values.chartURL);
} catch {
return;
}
const scheme = url.protocol;
const filename = url.pathname.split('/').pop() || '';
🤖 Prompt for AI Agents
In
`@frontend/packages/helm-plugin/src/components/forms/url-chart/HelmURLChartForm.tsx`
around lines 30 - 35, The effect that reads values.chartURL currently calls
URL.parse (not available in browsers); change it to construct a URL via new
URL(values.chartURL) inside a try/catch and return on error so invalid URLs
don't throw, then derive scheme from url.protocol and filename from url.pathname
as before; update the useEffect in HelmURLChartForm (the block referencing
values.chartURL, scheme, filename) to use this try/catch pattern and keep the
subsequent logic that auto-populates releaseName and chartVersion unchanged.

let chartName = '';
let chartVersion = '';

if (scheme === 'oci:') {
// e.g. "mychart:1.0.0" -> name "mychart", version "1.0.0"
[chartName, chartVersion] = filename.split(':');
} else {
// e.g. "exateapigator-0.1.0.tgz" -> name "exateapigator", version "0.1.0"
const base = filename.replace(/\.(tgz|tar\.gz)$/, '');

// Handle semVer cases like "my-chart-1.0.0-rc.1.tgz, if the last hyphen is followed by a digit"
const lastHyphen = base.lastIndexOf('-');
if (lastHyphen >= 0 && lastHyphen < base.length - 1 && base[lastHyphen + 1].match(/^\d/)) {
chartName = base.slice(0, lastHyphen);
chartVersion = base.slice(lastHyphen + 1);
}
Comment on lines +49 to +57
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.

Since you're using a regex at line 45, why not do the whole job with that?

Suggested change
// e.g. "exateapigator-0.1.0.tgz" -> name "exateapigator", version "0.1.0"
const base = filename.replace(/\.(tgz|tar\.gz)$/i, '');
const lastDash = base.lastIndexOf('-');
if (lastDash >= 0) {
chartName = base.slice(0, lastDash);
chartVersion = base.slice(lastDash + 1);
} else {
chartName = base;
}
// e.g. "exateapigator-0.1.0.tgz" -> name "exateapigator", version "0.1.0"
const matches = filename.match(/^(.*?)(?:-([^-]*))?\.(?:tgz|tar\.gz)$/i);
const chartName = matches ? matches[1] ?? "" : "";
const chartVersion = matches ? matches[2] ?? "" : "";

However, trying to pull the version out of the URL is not going to work in all cases: for instance, I believe that exateapigator-0.1.0-alpha.rc-1.tgz is a legal Helm package name (see the SemVer 2.0 spec). So, I don't think that we can extract the version from the URL, and I don't believe that Helm itself even tries. According to Cursor/gpt-5.2-codex, Helm uses the URL to download the chart package, extracts the Chart.yaml file, and pulls the version from the Metadata.Version field, rather than parsing the URL. (Normally, it would have stored this value in the Helm repository, but, in the case of installing from a URL, that's not available.)

So, I think we need to pause and reapproach this. I'm wondering if the backend can help.

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 have changed the approach, there is an attempt to extract the version number in the semver case, otherwise its just blank. User will have to input it themselves.

}

if (chartName) {
setFieldValue('releaseName', chartName);
}
if (chartVersion) {
setFieldValue('chartVersion', chartVersion);
}
}, [values.chartURL, setFieldValue, setFieldError, t]);

return (
<FlexForm
onSubmit={(e) => {
e.preventDefault();
onNext();
}}
>
<FormBody flexLayout>
<FormHeader
title={t('helm-plugin~Install Helm chart from URL')}
helpText={t(
'helm-plugin~To install a Helm chart, enter the chart URL - Open Container Initiative (OCI) URL or HTTP/HTTPS tar file and version.',
)}
marginBottom="lg"
/>
<FormSection fullWidth>
<Grid hasGutter>
<GridItem md={12}>
<InputField
type={TextInputTypes.text}
name="chartURL"
label={t('helm-plugin~Chart URL')}
helpText={t(
'helm-plugin~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.',
)}
placeholder="oci://registry.example.com/charts/mychart or https://example.com/chart-1.0.0.tgz"
required
data-test="oci-chart-url"
/>
</GridItem>
<GridItem md={6}>
<InputField
type={TextInputTypes.text}
name="releaseName"
label={t('helm-plugin~Release name')}
helpText={t('helm-plugin~Unique name for Helm release.')}
required
data-test="oci-release-name"
/>
</GridItem>
<GridItem md={6}>
<InputField
type={TextInputTypes.text}
name="chartVersion"
label={t('helm-plugin~Chart version')}
helpText={t('helm-plugin~The version of chart to install.')}
placeholder="1.0.0"
required
data-test="oci-chart-version"
/>
</GridItem>
</Grid>
</FormSection>
</FormBody>
<FormFooter
handleReset={handleReset}
errorMessage={status?.submitError}
isSubmitting={isSubmitting}
submitLabel={t('helm-plugin~Next')}
disableSubmit={isNextDisabled}
resetLabel={t('helm-plugin~Cancel')}
sticky
/>
</FlexForm>
);
};

export default HelmURLChartForm;
Loading