Skip to content

Commit a011c5e

Browse files
marcelofukumotobender
andauthored
Set kubectl.kubernetes.io/restartedAt annotation on workload redeploy (#18496)
Fixes #16445 Co-authored-by: bender <bender@local>
1 parent 467c5f5 commit a011c5e

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

shell/config/labels-annotations.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const NORMAN_NAME = 'field.cattle.io/name';
22
export const DESCRIPTION = 'field.cattle.io/description';
33
export const HOSTNAME = 'kubernetes.io/hostname';
44
export const TIMESTAMP = 'cattle.io/timestamp';
5+
export const KUBECTL_RESTARTED_AT = 'kubectl.kubernetes.io/restartedAt';
56
export const SYSTEM_NAMESPACE = 'management.cattle.io/system-namespace';
67
export const PROJECT = 'field.cattle.io/projectId';
78
export const DEFAULT_PROJECT = 'authz.management.cattle.io/default-project';

shell/dialog/RedeployWorkloadDialog.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { TIMESTAMP } from '@shell/config/labels-annotations';
2+
import { TIMESTAMP, KUBECTL_RESTARTED_AT } from '@shell/config/labels-annotations';
33
import AsyncButton from '@shell/components/AsyncButton';
44
import { Card } from '@components/Card';
55
import { Banner } from '@components/Banner';
@@ -83,6 +83,7 @@ export default {
8383
const annotations = metadata.annotations ??= {};
8484
8585
annotations[TIMESTAMP] = now;
86+
annotations[KUBECTL_RESTARTED_AT] = now;
8687
8788
await workload.save();
8889
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { shallowMount } from '@vue/test-utils';
2+
import RedeployWorkloadDialog from '@shell/dialog/RedeployWorkloadDialog.vue';
3+
import { TIMESTAMP, KUBECTL_RESTARTED_AT } from '@shell/config/labels-annotations';
4+
5+
const defaultStubs = {
6+
Card: true,
7+
AsyncButton: true,
8+
Banner: true,
9+
};
10+
11+
const defaultMocks = {
12+
$store: {
13+
getters: {
14+
'i18n/t': jest.fn((key: string) => key),
15+
'type-map/labelFor': jest.fn(() => 'Deployment'),
16+
},
17+
},
18+
t: jest.fn((key: string) => key),
19+
};
20+
21+
function createWorkload(overrides = {}) {
22+
return {
23+
nameDisplay: 'my-workload',
24+
type: 'apps.deployment',
25+
schema: { id: 'apps.deployment' },
26+
spec: { template: { metadata: { annotations: {} } } },
27+
save: jest.fn().mockResolvedValue(undefined),
28+
...overrides,
29+
};
30+
}
31+
32+
describe('component: RedeployWorkloadDialog', () => {
33+
const createWrapper = (propsData = {}, mocks = {}) => {
34+
return shallowMount(RedeployWorkloadDialog, {
35+
propsData: {
36+
workloads: [createWorkload()],
37+
...propsData,
38+
},
39+
global: {
40+
mocks: {
41+
...defaultMocks,
42+
...mocks,
43+
},
44+
stubs: defaultStubs,
45+
directives: { 'clean-html': true },
46+
},
47+
});
48+
};
49+
50+
beforeEach(() => {
51+
jest.clearAllMocks();
52+
});
53+
54+
it('should render correctly', () => {
55+
const wrapper = createWrapper();
56+
57+
expect(wrapper.exists()).toBe(true);
58+
});
59+
60+
describe('apply', () => {
61+
it('should set both TIMESTAMP and kubectl restartedAt annotations', async() => {
62+
const workload = createWorkload();
63+
const wrapper = createWrapper({ workloads: [workload] });
64+
const buttonDone = jest.fn();
65+
66+
await (wrapper.vm as any).apply(buttonDone);
67+
68+
const annotations = workload.spec.template.metadata.annotations;
69+
70+
expect(annotations[TIMESTAMP]).toBeDefined();
71+
expect(annotations[KUBECTL_RESTARTED_AT]).toBeDefined();
72+
expect(annotations[TIMESTAMP]).toStrictEqual(annotations[KUBECTL_RESTARTED_AT]);
73+
expect(workload.save).toHaveBeenCalledWith();
74+
expect(buttonDone).toHaveBeenCalledWith(true);
75+
});
76+
77+
it('should set annotations as ISO timestamps without milliseconds', async() => {
78+
const workload = createWorkload();
79+
const wrapper = createWrapper({ workloads: [workload] });
80+
81+
await (wrapper.vm as any).apply();
82+
83+
const ts = workload.spec.template.metadata.annotations[KUBECTL_RESTARTED_AT];
84+
85+
expect(ts).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
86+
});
87+
88+
it('should initialize metadata and annotations when missing', async() => {
89+
const workload = createWorkload({ spec: { template: {} } });
90+
const wrapper = createWrapper({ workloads: [workload] });
91+
92+
await (wrapper.vm as any).apply();
93+
94+
const annotations = workload.spec.template.metadata.annotations;
95+
96+
expect(annotations[TIMESTAMP]).toBeDefined();
97+
expect(annotations[KUBECTL_RESTARTED_AT]).toBeDefined();
98+
});
99+
100+
it('should redeploy all workloads', async() => {
101+
const workloads = [createWorkload(), createWorkload(), createWorkload()];
102+
const wrapper = createWrapper({ workloads });
103+
104+
await (wrapper.vm as any).apply();
105+
106+
for (const w of workloads) {
107+
expect(w.save).toHaveBeenCalledWith();
108+
expect(w.spec.template.metadata.annotations[TIMESTAMP]).toBeDefined();
109+
expect(w.spec.template.metadata.annotations[KUBECTL_RESTARTED_AT]).toBeDefined();
110+
}
111+
});
112+
113+
it('should report errors and call buttonDone(false) on failure', async() => {
114+
const workload = createWorkload({ save: jest.fn().mockRejectedValue(new Error('save failed')) });
115+
const wrapper = createWrapper({ workloads: [workload] });
116+
const buttonDone = jest.fn();
117+
118+
await (wrapper.vm as any).apply(buttonDone);
119+
120+
expect(buttonDone).toHaveBeenCalledWith(false);
121+
expect((wrapper.vm as any).errors.length).toBeGreaterThan(0);
122+
});
123+
});
124+
});

0 commit comments

Comments
 (0)