diff --git a/api/http/handler/helm/helm_install.go b/api/http/handler/helm/helm_install.go index 83ae0db51..33b0d82cd 100644 --- a/api/http/handler/helm/helm_install.go +++ b/api/http/handler/helm/helm_install.go @@ -46,18 +46,24 @@ var errChartNameInvalid = errors.New("invalid chart name. " + // @produce json // @param id path int true "Environment(Endpoint) identifier" // @param payload body installChartPayload true "Chart details" +// @param dryRun query bool false "Dry run" // @success 201 {object} release.Release "Created" // @failure 401 "Unauthorized" // @failure 404 "Environment(Endpoint) or ServiceAccount not found" // @failure 500 "Server error" // @router /endpoints/{id}/kubernetes/helm [post] func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + dryRun, err := request.RetrieveBooleanQueryParameter(r, "dryRun", true) + if err != nil { + return httperror.BadRequest("Invalid dryRun query parameter", err) + } + var payload installChartPayload if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { return httperror.BadRequest("Invalid Helm install payload", err) } - release, err := handler.installChart(r, payload) + release, err := handler.installChart(r, payload, dryRun) if err != nil { return httperror.InternalServerError("Unable to install a chart", err) } @@ -94,7 +100,7 @@ func (p *installChartPayload) Validate(_ *http.Request) error { return nil } -func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*release.Release, error) { +func (handler *Handler) installChart(r *http.Request, p installChartPayload, dryRun bool) (*release.Release, error) { clusterAccess, httperr := handler.getHelmClusterAccess(r) if httperr != nil { return nil, httperr.Err @@ -107,6 +113,7 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r Namespace: p.Namespace, Repo: p.Repo, Atomic: p.Atomic, + DryRun: dryRun, KubernetesClusterAccess: clusterAccess, } @@ -134,13 +141,14 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r return nil, err } - manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest) - if err != nil { - return nil, err - } - - if err := handler.updateHelmAppManifest(r, manifest, installOpts.Namespace); err != nil { - return nil, err + if !installOpts.DryRun { + manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest) + if err != nil { + return nil, err + } + if err := handler.updateHelmAppManifest(r, manifest, installOpts.Namespace); err != nil { + return nil, err + } } return release, nil diff --git a/app/react/components/CodeEditor/CodeEditor.test.tsx b/app/react/components/CodeEditor/CodeEditor.test.tsx index 7b100b0e3..a269cd192 100644 --- a/app/react/components/CodeEditor/CodeEditor.test.tsx +++ b/app/react/components/CodeEditor/CodeEditor.test.tsx @@ -122,7 +122,7 @@ test('should apply custom height', async () => { ); - const editor = (await findByRole('textbox')).parentElement?.parentElement; + const editor = await findByRole('textbox'); expect(editor).toHaveStyle({ height: customHeight }); }); diff --git a/app/react/components/CodeEditor/useCodeEditorExtensions.ts b/app/react/components/CodeEditor/useCodeEditorExtensions.ts index 3b46a543e..8050b59da 100644 --- a/app/react/components/CodeEditor/useCodeEditorExtensions.ts +++ b/app/react/components/CodeEditor/useCodeEditorExtensions.ts @@ -48,7 +48,7 @@ function yamlLanguage(schema?: JSONSchema7) { syntaxHighlighting(oneDarkHighlightStyle), // explicitly setting lineNumbers() as an extension ensures that the gutter order is the same between the diff viewer and the code editor lineNumbers(), - lintGutter(), + !!schema && lintGutter(), keymap.of([...defaultKeymap, ...completionKeymap, ...lintKeymap]), // only show completions when a schema is provided !!schema && diff --git a/app/react/components/form-components/FormSection/FormSection.tsx b/app/react/components/form-components/FormSection/FormSection.tsx index 6ea747762..51dbab534 100644 --- a/app/react/components/form-components/FormSection/FormSection.tsx +++ b/app/react/components/form-components/FormSection/FormSection.tsx @@ -12,6 +12,7 @@ interface Props { titleClassName?: string; className?: string; htmlFor?: string; + setIsDefaultFolded?: (isDefaultFolded: boolean) => void; } export function FormSection({ @@ -23,6 +24,7 @@ export function FormSection({ titleClassName, className, htmlFor = '', + setIsDefaultFolded, }: PropsWithChildren) { const [isExpanded, setIsExpanded] = useState(!defaultFolded); const id = `foldingButton${title}`; @@ -39,7 +41,10 @@ export function FormSection({ isExpanded={isExpanded} data-cy={id} id={id} - onClick={() => setIsExpanded((isExpanded) => !isExpanded)} + onClick={() => { + setIsExpanded((isExpanded) => !isExpanded); + setIsDefaultFolded?.(isExpanded); + }} /> )} diff --git a/app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts b/app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts index fcaf9df8f..8ab241048 100644 --- a/app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts +++ b/app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts @@ -7,7 +7,7 @@ import { getAllSettledItems } from '@/portainer/helpers/promise-utils'; import { withGlobalError } from '@/react-tools/react-query'; import { notifyError, notifySuccess } from '@/portainer/services/notifications'; import { pluralize } from '@/portainer/helpers/strings'; -import { uninstallHelmApplication } from '@/react/kubernetes/helm/HelmApplicationView/queries/useUninstallHelmAppMutation'; +import { uninstallHelmApplication } from '@/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation'; import { parseKubernetesAxiosError } from '../../axiosError'; import { ApplicationRowData } from '../ListView/ApplicationsDatatable/types'; diff --git a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/RollbackButton.tsx b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/RollbackButton.tsx index ea8a79bac..5fba1b1ae 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/RollbackButton.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/RollbackButton.tsx @@ -9,7 +9,7 @@ import { buildConfirmButton } from '@@/modals/utils'; import { confirm } from '@@/modals/confirm'; import { ModalType } from '@@/modals'; -import { useHelmRollbackMutation } from '../queries/useHelmRollbackMutation'; +import { useHelmRollbackMutation } from '../../helmReleaseQueries/useHelmRollbackMutation'; type Props = { latestRevision: number; diff --git a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UninstallButton.tsx b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UninstallButton.tsx index fc2c6d5c2..1d694532c 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UninstallButton.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UninstallButton.tsx @@ -5,7 +5,7 @@ import { notifySuccess } from '@/portainer/services/notifications'; import { DeleteButton } from '@@/buttons/DeleteButton'; -import { useUninstallHelmAppMutation } from '../queries/useUninstallHelmAppMutation'; +import { useUninstallHelmAppMutation } from '../../helmReleaseQueries/useUninstallHelmAppMutation'; export function UninstallButton({ environmentId, diff --git a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.test.tsx b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.test.tsx index f05a2d5e8..c5ae16622 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.test.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.test.tsx @@ -9,7 +9,7 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { useHelmRepoVersions, ChartVersion, -} from '../../queries/useHelmRepoVersions'; +} from '../../helmChartSourceQueries/useHelmRepoVersions'; import { HelmRelease } from '../../types'; import { openUpgradeHelmModal } from './UpgradeHelmModal'; @@ -25,32 +25,56 @@ vi.mock('@/portainer/services/notifications', () => ({ notifySuccess: vi.fn(), })); -vi.mock('../../queries/useHelmRepositories', () => ({ - useUserHelmRepositories: vi.fn(() => ({ - data: ['repo1', 'repo2'], - isInitialLoading: false, - isError: false, - })), -})); +vi.mock('../../helmChartSourceQueries/useHelmRepositories', async () => { + const actual = await vi.importActual( + '../../helmChartSourceQueries/useHelmRepositories' + ); + return { + ...actual, + useUserHelmRepositories: vi.fn(() => ({ + data: ['repo1', 'repo2'], + isInitialLoading: false, + isError: false, + })), + }; +}); -vi.mock('../../queries/useHelmRepoVersions', () => ({ - useHelmRepoVersions: vi.fn(), -})); +vi.mock('../../helmChartSourceQueries/useHelmRepoVersions', async () => { + const actual = await vi.importActual( + '../../helmChartSourceQueries/useHelmRepoVersions' + ); + return { + ...actual, + useHelmRepoVersions: vi.fn(), + }; +}); // Mock the useHelmRelease hook -vi.mock('../queries/useHelmRelease', () => ({ - useHelmRelease: vi.fn(() => ({ - data: '1.0.0', - })), -})); +vi.mock('../../helmReleaseQueries/useHelmRelease', async () => { + const actual = await vi.importActual( + '../../helmReleaseQueries/useHelmRelease' + ); + return { + ...actual, + useHelmRelease: vi.fn(() => ({ + data: '1.0.0', + })), + }; +}); // Mock the useUpdateHelmReleaseMutation hook -vi.mock('../../queries/useUpdateHelmReleaseMutation', () => ({ - useUpdateHelmReleaseMutation: vi.fn(() => ({ - mutate: vi.fn(), - isLoading: false, - })), -})); +vi.mock('../../helmReleaseQueries/useUpdateHelmReleaseMutation', async () => { + const actual = await vi.importActual( + '../../helmReleaseQueries/useUpdateHelmReleaseMutation' + ); + return { + ...actual, + useUpdateHelmReleaseMutation: vi.fn(() => ({ + mutate: vi.fn(), + isLoading: false, + })), + }; +}); function renderButton(props = {}) { const defaultProps = { @@ -157,12 +181,14 @@ describe('UpgradeButton', () => { metadata: { name: 'test-chart', version: '1.0.0', + appVersion: '1.0.0', }, }, values: { userSuppliedValues: '{}', }, manifest: '', + namespace: 'default', } as HelmRelease; vi.mocked(useHelmRepoVersions).mockReturnValue({ @@ -193,7 +219,9 @@ describe('UpgradeButton', () => { expect.arrayContaining([ { Version: '1.0.0', Repo: 'stable' }, { Version: '1.1.0', Repo: 'stable' }, - ]) + ]), + '', // releaseManifest + 1 // environmentId ); }); }); diff --git a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.tsx b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.tsx index 0e3e634fc..90d142394 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeButton.tsx @@ -12,10 +12,13 @@ import { Tooltip } from '@@/Tip/Tooltip'; import { Link } from '@@/Link'; import { HelmRelease, UpdateHelmReleasePayload } from '../../types'; -import { useUpdateHelmReleaseMutation } from '../../queries/useUpdateHelmReleaseMutation'; -import { useHelmRepoVersions } from '../../queries/useHelmRepoVersions'; -import { useHelmRelease } from '../queries/useHelmRelease'; -import { useUserHelmRepositories } from '../../queries/useHelmRepositories'; +import { useUpdateHelmReleaseMutation } from '../../helmReleaseQueries/useUpdateHelmReleaseMutation'; +import { useHelmRepoVersions } from '../../helmChartSourceQueries/useHelmRepoVersions'; +import { useHelmRelease } from '../../helmReleaseQueries/useHelmRelease'; +import { + flattenHelmRegistries, + useUserHelmRepositories, +} from '../../helmChartSourceQueries/useHelmRepositories'; import { openUpgradeHelmModal } from './UpgradeHelmModal'; @@ -36,7 +39,9 @@ export function UpgradeButton({ const [useCache, setUseCache] = useState(true); const updateHelmReleaseMutation = useUpdateHelmReleaseMutation(environmentId); - const userRepositoriesQuery = useUserHelmRepositories(); + const userRepositoriesQuery = useUserHelmRepositories({ + select: flattenHelmRegistries, + }); const helmRepoVersionsQuery = useHelmRepoVersions( release?.chart.metadata?.name || '', 60 * 60 * 1000, // 1 hour @@ -164,7 +169,9 @@ export function UpgradeButton({ async function handleUpgrade() { const submittedUpgradeValues = await openUpgradeHelmModal( editableHelmRelease, - filteredVersions + filteredVersions, + release?.manifest || '', + environmentId ); if (submittedUpgradeValues) { diff --git a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeHelmModal.tsx b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeHelmModal.tsx index a1382bfd3..7be9f0b9c 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeHelmModal.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ChartActions/UpgradeHelmModal.tsx @@ -1,9 +1,10 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { ArrowUp } from 'lucide-react'; import { withReactQuery } from '@/react-tools/withReactQuery'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; -import { ChartVersion } from '@/react/kubernetes/helm/queries/useHelmRepoVersions'; +import { ChartVersion } from '@/react/kubernetes/helm/helmChartSourceQueries/useHelmRepoVersions'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { Modal, OnSubmit, openModal } from '@@/modals'; import { Button } from '@@/buttons'; @@ -15,26 +16,32 @@ import { Option, PortainerSelect } from '@@/form-components/PortainerSelect'; import { UpdateHelmReleasePayload } from '../../types'; import { HelmValuesInput } from '../../components/HelmValuesInput'; -import { useHelmChartValues } from '../../queries/useHelmChartValues'; +import { useHelmChartValues } from '../../helmChartSourceQueries/useHelmChartValues'; +import { ManifestPreviewFormSection } from '../../components/ManifestPreviewFormSection'; interface Props { onSubmit: OnSubmit; - payload: UpdateHelmReleasePayload; + helmReleaseInitialValues: UpdateHelmReleasePayload; + releaseManifest: string; versions: ChartVersion[]; chartName: string; + environmentId: EnvironmentId; } export function UpgradeHelmModal({ - payload, + helmReleaseInitialValues, + releaseManifest, versions, onSubmit, chartName, + environmentId, }: Props) { const versionOptions: Option[] = versions.map((version) => { - const repo = payload.repo === version.Repo ? version.Repo : ''; + const repo = + helmReleaseInitialValues.repo === version.Repo ? version.Repo : ''; const isCurrentVersion = - version.AppVersion === payload.appVersion && - version.Version === payload.version; + version.AppVersion === helmReleaseInitialValues.appVersion && + version.Version === helmReleaseInitialValues.version; const label = `${repo}@${version.Version}${ isCurrentVersion ? ' (current)' : '' @@ -50,13 +57,16 @@ export function UpgradeHelmModal({ const defaultVersion = versionOptions.find( (v) => - v.value.AppVersion === payload.appVersion && - v.value.Version === payload.version && - v.value.Repo === payload.repo + v.value.AppVersion === helmReleaseInitialValues.appVersion && + v.value.Version === helmReleaseInitialValues.version && + v.value.Repo === helmReleaseInitialValues.repo )?.value || versionOptions[0]?.value; const [version, setVersion] = useState(defaultVersion); - const [userValues, setUserValues] = useState(payload.values || ''); + const [userValues, setUserValues] = useState( + helmReleaseInitialValues.values || '' + ); const [atomic, setAtomic] = useState(true); + const [previewIsValid, setPreviewIsValid] = useState(false); const chartValuesRefQuery = useHelmChartValues({ chart: chartName, @@ -64,6 +74,19 @@ export function UpgradeHelmModal({ version: version.Version, }); + const submitPayload = useMemo( + () => ({ + name: helmReleaseInitialValues.name, + values: userValues, + namespace: helmReleaseInitialValues.namespace, + chart: helmReleaseInitialValues.chart, + repo: version.Repo, + version: version.Version, + atomic, + }), + [helmReleaseInitialValues, userValues, version, atomic] + ); + return ( onSubmit()} @@ -84,7 +107,7 @@ export function UpgradeHelmModal({ > +
+ +
@@ -149,17 +181,8 @@ export function UpgradeHelmModal({ Cancel