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:
parent
4ee349bd6b
commit
d49fcd8f3e
11 changed files with 71 additions and 10 deletions
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue