1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 13:29:41 +02:00

feat(helm): make the atomic flag optional [r8s-314] (#733)

This commit is contained in:
Ali 2025-05-14 16:31:42 +12:00 committed by GitHub
parent 4ee349bd6b
commit d49fcd8f3e
11 changed files with 71 additions and 10 deletions

View file

@ -27,6 +27,7 @@ type installChartPayload struct {
Repo string `json:"repo"` Repo string `json:"repo"`
Values string `json:"values"` Values string `json:"values"`
Version string `json:"version"` Version string `json:"version"`
Atomic bool `json:"atomic"`
} }
var errChartNameInvalid = errors.New("invalid chart name. " + var errChartNameInvalid = errors.New("invalid chart name. " +
@ -105,6 +106,7 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
Version: p.Version, Version: p.Version,
Namespace: p.Namespace, Namespace: p.Namespace,
Repo: p.Repo, Repo: p.Repo,
Atomic: p.Atomic,
KubernetesClusterAccess: clusterAccess, KubernetesClusterAccess: clusterAccess,
} }

View file

@ -11,6 +11,7 @@ import { Input } from '@@/form-components/Input';
import { CodeEditor } from '@@/CodeEditor'; import { CodeEditor } from '@@/CodeEditor';
import { FormControl } from '@@/form-components/FormControl'; import { FormControl } from '@@/form-components/FormControl';
import { WidgetTitle } from '@@/Widget'; import { WidgetTitle } from '@@/Widget';
import { Checkbox } from '@@/form-components/Checkbox';
import { UpdateHelmReleasePayload } from '../queries/useUpdateHelmReleaseMutation'; import { UpdateHelmReleasePayload } from '../queries/useUpdateHelmReleaseMutation';
import { ChartVersion } from '../queries/useHelmRepositories'; import { ChartVersion } from '../queries/useHelmRepositories';
@ -37,7 +38,7 @@ export function UpgradeHelmModal({ values, versions, onSubmit }: Props) {
versionOptions[0]?.value; versionOptions[0]?.value;
const [version, setVersion] = useState<ChartVersion>(defaultVersion); const [version, setVersion] = useState<ChartVersion>(defaultVersion);
const [userValues, setUserValues] = useState<string>(values.values || ''); const [userValues, setUserValues] = useState<string>(values.values || '');
const [atomic, setAtomic] = useState<boolean>(false);
return ( return (
<Modal <Modal
onDismiss={() => onSubmit()} onDismiss={() => onSubmit()}
@ -88,6 +89,20 @@ export function UpgradeHelmModal({ values, versions, onSubmit }: Props) {
data-cy="helm-namespace-input" data-cy="helm-namespace-input"
/> />
</FormControl> </FormControl>
<FormControl
label="Rollback on failure"
tooltip="Enables automatic rollback on failure (equivalent to the helm --atomic flag). It may increase the time to upgrade."
inputId="atomic-input"
className="[&>label]:!pl-0"
size="medium"
>
<Checkbox
id="atomic-input"
checked={atomic}
data-cy="atomic-checkbox"
onChange={(e) => setAtomic(e.target.checked)}
/>
</FormControl>
<FormControl <FormControl
label="User-defined values" label="User-defined values"
inputId="user-values-editor" inputId="user-values-editor"
@ -125,6 +140,7 @@ export function UpgradeHelmModal({ values, versions, onSubmit }: Props) {
chart: values.chart, chart: values.chart,
repo: version.Repo, repo: version.Repo,
version: version.Version, version: version.Version,
atomic,
}) })
} }
color="primary" color="primary"

View file

@ -218,7 +218,9 @@ describe('HelmEventsDatatable', () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText('Events reflect the latest revision only.') screen.getByText(
'Only events for resources currently in the cluster will be displayed.'
)
).toBeInTheDocument(); ).toBeInTheDocument();
}); });

View file

@ -42,7 +42,8 @@ export function HelmEventsDatatable({
dataset={eventsQuery.data || []} dataset={eventsQuery.data || []}
title={ title={
<TextTip inline color="blue" className="!text-xs"> <TextTip inline color="blue" className="!text-xs">
Events reflect the latest revision only. Only events for resources currently in the cluster will be
displayed.
</TextTip> </TextTip>
} }
titleIcon={null} titleIcon={null}

View file

@ -167,9 +167,13 @@ describe('ResourcesTable', () => {
); );
// Check that success badge is rendered // Check that success badge is rendered
const successBadge = screen.getByText('MinimumReplicasAvailable'); const successBadge = screen.getByText(
(content, element) =>
content.includes('MinimumReplicasAvailable') &&
element !== null &&
element.className.includes('bg-success')
);
expect(successBadge).toBeInTheDocument(); expect(successBadge).toBeInTheDocument();
expect(successBadge.className).toContain('bg-success');
}); });
it('should show error badges for failed resources', () => { it('should show error badges for failed resources', () => {
@ -177,8 +181,12 @@ describe('ResourcesTable', () => {
expect(screen.getByText('probe-failure-nginx-bad')).toBeInTheDocument(); expect(screen.getByText('probe-failure-nginx-bad')).toBeInTheDocument();
// Check for the unhealthy status badge and make sure it has the error styling // Check for the unhealthy status badge and make sure it has the error styling
const errorBadge = screen.getByText('InsufficientPods'); const errorBadge = screen.getByText(
(content, element) =>
content.includes('InsufficientPods') &&
element !== null &&
element.className.includes('bg-error')
);
expect(errorBadge).toBeInTheDocument(); expect(errorBadge).toBeInTheDocument();
expect(errorBadge.className).toContain('bg-error');
}); });
}); });

View file

@ -59,7 +59,7 @@ export function ResourcesTable() {
emptyContentLabel="No resources found" emptyContentLabel="No resources found"
title={ title={
<TextTip inline color="blue" className="!text-xs"> <TextTip inline color="blue" className="!text-xs">
Resources reflect the latest revision only. Only resources currently in the cluster will be displayed.
</TextTip> </TextTip>
} }
disableSelect disableSelect

View file

@ -1,6 +1,20 @@
import { Row } from '@tanstack/react-table';
import { filterHOC } from '@@/datatables/Filter';
import { ResourceRow } from '../types';
import { columnHelper } from './helper'; import { columnHelper } from './helper';
export const resourceType = columnHelper.accessor((row) => row.resourceType, { export const resourceType = columnHelper.accessor((row) => row.resourceType, {
header: 'Resource type', header: 'Resource type',
id: 'resourceType', id: 'resourceType',
meta: {
filter: filterHOC('Filter by resource type'),
},
enableColumnFilter: true,
filterFn: (row: Row<ResourceRow>, _: string, filterValue: string[]) =>
filterValue.length === 0 ||
(!!row.original.resourceType &&
filterValue.includes(row.original.resourceType)),
}); });

View file

@ -1,6 +1,7 @@
import { CellContext } from '@tanstack/react-table'; import { CellContext, Row } from '@tanstack/react-table';
import { StatusBadge } from '@@/StatusBadge'; import { StatusBadge } from '@@/StatusBadge';
import { filterHOC } from '@@/datatables/Filter';
import { ResourceRow } from '../types'; import { ResourceRow } from '../types';
@ -10,6 +11,21 @@ export const status = columnHelper.accessor((row) => row.status.label, {
header: 'Status', header: 'Status',
id: 'status', id: 'status',
cell: Cell, cell: Cell,
meta: {
filter: filterHOC(
'Filter by status',
// don't include empty values in the filter options
(rows: Row<ResourceRow>[]) =>
Array.from(
new Set(rows.map((row) => row.original.status.label).filter(Boolean))
)
),
},
enableColumnFilter: true,
filterFn: (row: Row<ResourceRow>, _: string, filterValue: string[]) =>
filterValue.length === 0 ||
(!!row.original.status.label &&
filterValue.includes(row.original.status.label)),
}); });
function Cell({ row }: CellContext<ResourceRow, string>) { function Cell({ row }: CellContext<ResourceRow, string>) {

View file

@ -14,6 +14,7 @@ export interface UpdateHelmReleasePayload {
name: string; name: string;
chart: string; chart: string;
version?: string; version?: string;
atomic?: boolean;
} }
export function useUpdateHelmReleaseMutation(environmentId: EnvironmentId) { export function useUpdateHelmReleaseMutation(environmentId: EnvironmentId) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View file

@ -11,6 +11,7 @@ type InstallOptions struct {
Wait bool Wait bool
ValuesFile string ValuesFile string
PostRenderer string PostRenderer string
Atomic bool
Timeout time.Duration Timeout time.Duration
KubernetesClusterAccess *KubernetesClusterAccess KubernetesClusterAccess *KubernetesClusterAccess

View file

@ -133,7 +133,7 @@ func (hspm *HelmSDKPackageManager) Upgrade(upgradeOpts options.InstallOptions) (
func initUpgradeClient(actionConfig *action.Configuration, upgradeOpts options.InstallOptions) (*action.Upgrade, error) { func initUpgradeClient(actionConfig *action.Configuration, upgradeOpts options.InstallOptions) (*action.Upgrade, error) {
upgradeClient := action.NewUpgrade(actionConfig) upgradeClient := action.NewUpgrade(actionConfig)
upgradeClient.DependencyUpdate = true upgradeClient.DependencyUpdate = true
upgradeClient.Atomic = true upgradeClient.Atomic = upgradeOpts.Atomic
upgradeClient.ChartPathOptions.RepoURL = upgradeOpts.Repo upgradeClient.ChartPathOptions.RepoURL = upgradeOpts.Repo
upgradeClient.Wait = upgradeOpts.Wait upgradeClient.Wait = upgradeOpts.Wait