Skip to content

Commit 123b10a

Browse files
authored
Merge pull request #4665 from alokdangre/feat/rollout-undo-deployment
frontend: workloads: Implement rollback for Deployment, DaemonSet, StatefulSet
2 parents 3596664 + 4bc2a2d commit 123b10a

38 files changed

+1100
-8
lines changed

frontend/src/components/App/icons.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ const mdiIcons = {
228228
upload: {
229229
body: '\u003Cpath fill="currentColor" d="M9 16v-6H5l7-7l7 7h-4v6zm-4 4v-2h14v2z"/\u003E',
230230
},
231+
'undo-variant': {
232+
body: '\u003Cpath fill="currentColor" d="M13.5 21a7 7 0 0 0 7-7a7 7 0 0 0-7-7H6.41l2.59-2.59L7.59 3L2 8.59L7.59 14.17l1.41-1.41L6.41 10H13.5a5 5 0 0 1 5 5a5 5 0 0 1-5 5"/\u003E',
233+
},
231234
broom: {
232235
body: '\u003Cpath fill="currentColor" d="m19.36 2.72l1.42 1.42l-5.72 5.71c1.07 1.54 1.22 3.39.32 4.59L9.06 8.12c1.2-.9 3.05-.75 4.59.32zM5.93 17.57c-2.01-2.01-3.24-4.41-3.58-6.65l4.88-2.09l7.44 7.44l-2.09 4.88c-2.24-.34-4.64-1.57-6.65-3.58"/\u003E',
233236
},
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Meta, StoryFn } from '@storybook/react';
18+
import { getTestDate } from '../../../helpers/testHelpers';
19+
import DaemonSet from '../../../lib/k8s/daemonSet';
20+
import Deployment from '../../../lib/k8s/deployment';
21+
import StatefulSet from '../../../lib/k8s/statefulSet';
22+
import { TestContext } from '../../../test';
23+
import { RollbackButton } from './RollbackButton';
24+
25+
export default {
26+
title: 'Resource/RollbackButton',
27+
component: RollbackButton,
28+
decorators: [
29+
Story => (
30+
<TestContext>
31+
<Story />
32+
</TestContext>
33+
),
34+
],
35+
} as Meta;
36+
37+
const Template: StoryFn<typeof RollbackButton> = args => <RollbackButton {...args} />;
38+
39+
// Deployment example
40+
const mockDeployment = new Deployment({
41+
metadata: {
42+
name: 'frontend-app',
43+
namespace: 'production',
44+
creationTimestamp: getTestDate().toDateString(),
45+
uid: 'mock-uid-deployment',
46+
annotations: {
47+
'deployment.kubernetes.io/revision': '5',
48+
},
49+
},
50+
spec: {
51+
replicas: 3,
52+
selector: {
53+
matchLabels: { app: 'frontend-app' },
54+
},
55+
template: {
56+
spec: {
57+
nodeName: 'worker-node-1',
58+
containers: [
59+
{
60+
name: 'frontend',
61+
image: 'myapp:v1.3.0',
62+
ports: [{ containerPort: 80 }],
63+
imagePullPolicy: 'Always',
64+
},
65+
],
66+
},
67+
},
68+
},
69+
status: {
70+
replicas: 3,
71+
readyReplicas: 3,
72+
availableReplicas: 3,
73+
updatedReplicas: 3,
74+
},
75+
kind: 'Deployment',
76+
});
77+
78+
// DaemonSet example
79+
const mockDaemonSet = new DaemonSet({
80+
metadata: {
81+
name: 'node-exporter',
82+
namespace: 'monitoring',
83+
creationTimestamp: getTestDate().toDateString(),
84+
uid: 'mock-uid-daemonset',
85+
},
86+
spec: {
87+
selector: {
88+
matchLabels: { app: 'node-exporter' },
89+
},
90+
updateStrategy: {
91+
type: 'RollingUpdate',
92+
rollingUpdate: {
93+
maxUnavailable: 1,
94+
},
95+
},
96+
template: {
97+
spec: {
98+
nodeName: 'worker-node-1',
99+
containers: [
100+
{
101+
name: 'node-exporter',
102+
image: 'prom/node-exporter:v1.5.0',
103+
ports: [{ containerPort: 9100 }],
104+
imagePullPolicy: 'Always',
105+
},
106+
],
107+
},
108+
},
109+
},
110+
status: {
111+
currentNumberScheduled: 3,
112+
desiredNumberScheduled: 3,
113+
numberReady: 3,
114+
observedGeneration: 4,
115+
},
116+
kind: 'DaemonSet',
117+
});
118+
119+
// StatefulSet example
120+
const mockStatefulSet = new StatefulSet({
121+
metadata: {
122+
name: 'database',
123+
namespace: 'production',
124+
creationTimestamp: getTestDate().toDateString(),
125+
uid: 'mock-uid-statefulset',
126+
},
127+
spec: {
128+
replicas: 3,
129+
selector: {
130+
matchLabels: { app: 'database' },
131+
},
132+
updateStrategy: {
133+
type: 'RollingUpdate',
134+
rollingUpdate: { partition: 0 },
135+
},
136+
template: {
137+
spec: {
138+
nodeName: 'worker-node-1',
139+
containers: [
140+
{
141+
name: 'postgres',
142+
image: 'postgres:15',
143+
ports: [{ containerPort: 5432 }],
144+
imagePullPolicy: 'Always',
145+
},
146+
],
147+
},
148+
},
149+
},
150+
status: {
151+
replicas: 3,
152+
readyReplicas: 3,
153+
observedGeneration: 3,
154+
},
155+
kind: 'StatefulSet',
156+
});
157+
158+
/**
159+
* Default example showing a Deployment that can be rolled back.
160+
* The button shows the history icon and opens a confirmation dialog when clicked.
161+
*/
162+
export const DeploymentExample = Template.bind({});
163+
DeploymentExample.args = {
164+
item: mockDeployment,
165+
};
166+
167+
/**
168+
* Example showing a DaemonSet that can be rolled back.
169+
* DaemonSets use ControllerRevisions for revision history.
170+
*/
171+
export const DaemonSetExample = Template.bind({});
172+
DaemonSetExample.args = {
173+
item: mockDaemonSet,
174+
};
175+
176+
/**
177+
* Example showing a StatefulSet that can be rolled back.
178+
* StatefulSets also use ControllerRevisions for revision history.
179+
*/
180+
export const StatefulSetExample = Template.bind({});
181+
StatefulSetExample.args = {
182+
item: mockStatefulSet,
183+
};
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useState } from 'react';
18+
import { useTranslation } from 'react-i18next';
19+
import { useDispatch } from 'react-redux';
20+
import { useLocation } from 'react-router';
21+
import DaemonSet from '../../../lib/k8s/daemonSet';
22+
import Deployment from '../../../lib/k8s/deployment';
23+
import { KubeObject } from '../../../lib/k8s/KubeObject';
24+
import { RollbackResult } from '../../../lib/k8s/rollback';
25+
import StatefulSet from '../../../lib/k8s/statefulSet';
26+
import { clusterAction } from '../../../redux/clusterActionSlice';
27+
import {
28+
EventStatus,
29+
HeadlampEventType,
30+
useEventCallback,
31+
} from '../../../redux/headlampEventSlice';
32+
import { AppDispatch } from '../../../redux/stores/store';
33+
import ActionButton, { ButtonStyle } from '../ActionButton';
34+
import ConfirmDialog from '../ConfirmDialog';
35+
import AuthVisible from './AuthVisible';
36+
37+
/**
38+
* Interface for resources that support rollback.
39+
*/
40+
export interface RollbackableResource extends KubeObject {
41+
rollback: () => Promise<RollbackResult>;
42+
}
43+
44+
/**
45+
* Type guard to check if a KubeObject is a resource that supports rollback.
46+
* Currently supports Deployment, DaemonSet, and StatefulSet.
47+
*/
48+
export function isRollbackableResource(item: KubeObject): item is RollbackableResource {
49+
return Deployment.isClassOf(item) || DaemonSet.isClassOf(item) || StatefulSet.isClassOf(item);
50+
}
51+
52+
export interface RollbackButtonProps {
53+
/** The Kubernetes resource to rollback */
54+
item: KubeObject;
55+
/** Optional button style override */
56+
buttonStyle?: ButtonStyle;
57+
/** Optional callback after user confirms the rollback */
58+
afterConfirm?: () => void;
59+
}
60+
61+
/**
62+
* RollbackButton component for rolling back Workloads to their previous revision.
63+
*
64+
* This component provides a button that, when clicked, shows a confirmation dialog
65+
* and then initiates a rollback operation.
66+
*
67+
* Supported Resources:
68+
* - Deployment (via ReplicaSets)
69+
* - DaemonSet (via ControllerRevisions)
70+
* - StatefulSet (via ControllerRevisions)
71+
*/
72+
export function RollbackButton(props: RollbackButtonProps) {
73+
const dispatch: AppDispatch = useDispatch();
74+
const { item, buttonStyle, afterConfirm } = props;
75+
76+
if (!item || !isRollbackableResource(item)) {
77+
return null;
78+
}
79+
80+
const [openDialog, setOpenDialog] = useState(false);
81+
const location = useLocation();
82+
const { t } = useTranslation(['translation']);
83+
const dispatchRollbackEvent = useEventCallback(HeadlampEventType.ROLLBACK_RESOURCE);
84+
85+
const resource = item;
86+
const resourceKind = resource.kind;
87+
88+
async function performRollback() {
89+
const result = await resource.rollback();
90+
if (!result.success) {
91+
throw new Error(result.message);
92+
}
93+
return result;
94+
}
95+
96+
function handleConfirm() {
97+
const itemName = resource.metadata.name;
98+
99+
dispatch(
100+
clusterAction(() => performRollback(), {
101+
startMessage: t('Rolling back {{ itemName }} to previous version…', { itemName }),
102+
cancelledMessage: t('Cancelled rollback of {{ itemName }}.', { itemName }),
103+
successMessage: t('Rolled back {{ itemName }} to previous version.', { itemName }),
104+
errorMessage: t('Failed to rollback {{ itemName }}.', { itemName }),
105+
cancelUrl: location.pathname,
106+
startUrl: resource.getDetailsLink(),
107+
errorUrl: resource.getDetailsLink(),
108+
})
109+
);
110+
111+
setOpenDialog(false);
112+
113+
// Dispatch event for plugins/tracking
114+
dispatchRollbackEvent({
115+
resource: resource,
116+
status: EventStatus.CONFIRMED,
117+
});
118+
119+
if (afterConfirm) {
120+
afterConfirm();
121+
}
122+
}
123+
124+
return (
125+
<AuthVisible
126+
item={item}
127+
authVerb="update"
128+
onError={(err: Error) => {
129+
console.error(`Error while getting authorization for rollback button in ${item}:`, err);
130+
}}
131+
>
132+
<ActionButton
133+
description={t('translation|Rollback')}
134+
buttonStyle={buttonStyle}
135+
onClick={() => {
136+
setOpenDialog(true);
137+
}}
138+
icon="mdi:undo-variant"
139+
/>
140+
<ConfirmDialog
141+
open={openDialog}
142+
title={t('translation|Rollback {{ kind }}', { kind: resourceKind })}
143+
description={t(
144+
'translation|Are you sure you want to rollback "{{ name }}" to the previous version? This will replace the current pod template with the one from the previous revision.',
145+
{
146+
name: resource.metadata.name,
147+
}
148+
)}
149+
handleClose={() => setOpenDialog(false)}
150+
onConfirm={handleConfirm}
151+
cancelLabel={t('Cancel')}
152+
confirmLabel={t('Rollback')}
153+
/>
154+
</AuthVisible>
155+
);
156+
}
157+
158+
export default RollbackButton;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<body>
2+
<div>
3+
<button
4+
aria-label="Rollback"
5+
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
6+
data-mui-internal-clone-element="true"
7+
tabindex="0"
8+
type="button"
9+
>
10+
<span
11+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
12+
/>
13+
</button>
14+
<div />
15+
</div>
16+
</body>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<body>
2+
<div>
3+
<button
4+
aria-label="Rollback"
5+
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
6+
data-mui-internal-clone-element="true"
7+
tabindex="0"
8+
type="button"
9+
>
10+
<span
11+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
12+
/>
13+
</button>
14+
<div />
15+
</div>
16+
</body>

0 commit comments

Comments
 (0)