Skip to content

Commit 6fbd103

Browse files
authored
Fix runAsUser and fsGroup validation error when clearing fields (#17450)
* Drop empty runAsUser and fsGroup from emitted security context Clearing the field left an empty string in the spec; Kubernetes then rejected the save with "cannot unmarshal string ... of type int64". Also use a numeric input with min=0 for both fields so the empty-string path is harder to hit and non-numeric input is blocked at the UI. Fixes #9601 * Also delete fsGroup and runAsUser when undefined The parent security context object may not include these fields at all, leaving them as undefined after the spread in data(). The existing empty-string and null guards did not cover this case.
1 parent 2350970 commit 6fbd103

2 files changed

Lines changed: 82 additions & 2 deletions

File tree

shell/components/form/Security.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,12 @@ export default {
140140
this.afterPrivilegedTickedMessage = this.t('workload.container.security.privileged.afterTick.false');
141141
}
142142
143-
if (securityContext.fsGroup === '') {
143+
// Drop empty values so we don't send a string for int64 fields.
144+
if (securityContext.fsGroup === '' || securityContext.fsGroup === null || securityContext.fsGroup === undefined) {
144145
delete securityContext.fsGroup;
145146
}
146147
147-
if (securityContext.runAsUser === '') {
148+
if (securityContext.runAsUser === '' || securityContext.runAsUser === null || securityContext.runAsUser === undefined) {
148149
delete securityContext.runAsUser;
149150
}
150151
@@ -175,6 +176,7 @@ export default {
175176
ref="firstFocusable"
176177
v-model:value.number="securityContext.fsGroup"
177178
type="number"
179+
min="0"
178180
:mode="mode"
179181
:label="t('workload.container.security.fsGroup')"
180182
@update:value="update"
@@ -283,6 +285,8 @@ export default {
283285
</legend>
284286
<LabeledInput
285287
v-model:value.number="securityContext.runAsUser"
288+
type="number"
289+
min="0"
286290
:label="t('workload.container.security.runAsUser.label')"
287291
:mode="mode"
288292
@update:value="update"

shell/components/form/__tests__/Security.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ describe('component: Security', () => {
7979
expect(wrapper.emitted('update:value')).toHaveLength(1);
8080
});
8181

82+
// Regression: clearing runAsUser must drop the key from the emitted object.
83+
// Otherwise the spec is sent with `runAsUser: ""` and the API rejects with
84+
// "cannot unmarshal string ... of type int64". See issue #9601.
85+
it('should omit runAsUser from the emitted value when the input is cleared', async() => {
86+
const wrapper = mount(Security, {
87+
props: {
88+
mode: _EDIT, formType: FORM_TYPES.CONTAINER, value: { runAsUser: 33 }
89+
}
90+
});
91+
const input = wrapper.find('[data-testid="input-security-runAsUser"]').find('input');
92+
93+
await input.setValue('');
94+
95+
const events = wrapper.emitted('update:value') ?? [];
96+
const last = events[events.length - 1][0] as Record<string, unknown>;
97+
98+
expect(Object.prototype.hasOwnProperty.call(last, 'runAsUser')).toBe(false);
99+
});
100+
101+
it('should omit runAsUser from the emitted value when it is undefined', async() => {
102+
const wrapper = mount(Security, {
103+
props: {
104+
mode: _EDIT, formType: FORM_TYPES.CONTAINER, value: {}
105+
}
106+
});
107+
108+
const checkbox = wrapper.find('[data-testid="input-security-runasNonRoot"]').find('label');
109+
110+
await checkbox.trigger('click');
111+
112+
const events = wrapper.emitted('update:value') ?? [];
113+
const last = events[events.length - 1][0] as Record<string, unknown>;
114+
115+
expect(Object.prototype.hasOwnProperty.call(last, 'runAsUser')).toBe(false);
116+
});
117+
82118
it.each([
83119
'privileged',
84120
'allowPrivilegeEscalation',
@@ -139,5 +175,45 @@ describe('component: Security', () => {
139175

140176
expect(wrapper.emitted('update:value')).toHaveLength(1);
141177
});
178+
179+
it.each([
180+
'runAsUser',
181+
'fsGroup',
182+
])('should omit %p from the emitted value when it is undefined', async(field) => {
183+
const wrapper = mount(Security, {
184+
props: {
185+
mode: _EDIT, formType: FORM_TYPES.POD, value: {}
186+
}
187+
});
188+
189+
const checkbox = wrapper.find('[data-testid="input-security-runasNonRoot"]').find('label');
190+
191+
await checkbox.trigger('click');
192+
193+
const events = wrapper.emitted('update:value') ?? [];
194+
const last = events[events.length - 1][0] as Record<string, unknown>;
195+
196+
expect(Object.prototype.hasOwnProperty.call(last, field)).toBe(false);
197+
});
198+
199+
// Regression for #9601 — see equivalent container-level test above.
200+
it.each([
201+
['runAsUser', { runAsUser: 33 }],
202+
['fsGroup', { fsGroup: 100 }],
203+
])('should omit %p from the emitted value when the input is cleared', async(field, initial) => {
204+
const wrapper = mount(Security, {
205+
props: {
206+
mode: _EDIT, formType: FORM_TYPES.POD, value: initial
207+
}
208+
});
209+
const input = wrapper.find(`[data-testid="input-security-${ field }"]`).find('input');
210+
211+
await input.setValue('');
212+
213+
const events = wrapper.emitted('update:value') ?? [];
214+
const last = events[events.length - 1][0] as Record<string, unknown>;
215+
216+
expect(Object.prototype.hasOwnProperty.call(last, field)).toBe(false);
217+
});
142218
});
143219
});

0 commit comments

Comments
 (0)