Skip to content

Commit 70e0c45

Browse files
committed
Allow removing default backend by selecting None
Default backend validation rules now only enforce required checks when a service is actually selected. Selecting "None" skips validation so the user can save, and willSave() cleans up the empty backend object.
1 parent 760f7e7 commit 70e0c45

2 files changed

Lines changed: 162 additions & 27 deletions

File tree

shell/edit/networking.k8s.io.ingress/index.vue

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ export default {
9494
},
9595
{ path: 'spec', rules: ['backEndOrRules'] },
9696
{
97-
path: 'spec.defaultBackend.service.name', rules: ['required'], translationKey: 'ingress.defaultBackend.targetService.label'
97+
path: 'spec.defaultBackend.service.name', rules: ['defaultBackendNameRequired'], translationKey: 'ingress.defaultBackend.targetService.label'
9898
},
9999
{
100-
path: 'spec.defaultBackend.service.port', rules: ['portRequired', 'portRange'], translationKey: 'ingress.defaultBackend.port.label'
100+
path: 'spec.defaultBackend.service.port', rules: ['defaultBackendPortRequired', 'portRange'], translationKey: 'ingress.defaultBackend.port.label'
101101
},
102102
{ path: 'spec.tls.hosts', rules: ['required', 'wildcardHostname'] }
103103
],
@@ -156,8 +156,36 @@ export default {
156156
}
157157
};
158158
159+
const hasDefaultBackendService = () => {
160+
const backend = get(this.value?.spec, this.value.defaultBackendPath);
161+
162+
return !!get(backend, this.value.serviceNamePath);
163+
};
164+
165+
const nameLabel = this.t('ingress.defaultBackend.targetService.label');
166+
167+
// Only enforce required on the default backend when a service is selected.
168+
// Selecting "None" means the user wants to remove the backend; willSave() handles cleanup.
169+
const defaultBackendNameRequired = (name) => {
170+
if (hasDefaultBackendService() && !name) {
171+
return this.t('validation.required', { key: nameLabel });
172+
}
173+
};
174+
175+
const defaultBackendPortRequired = (port) => {
176+
if (!hasDefaultBackendService()) {
177+
return;
178+
}
179+
180+
return portRequired(port);
181+
};
182+
159183
return {
160-
backEndOrRules, portRequired, portRange
184+
backEndOrRules,
185+
portRequired,
186+
portRange,
187+
defaultBackendNameRequired,
188+
defaultBackendPortRequired,
161189
};
162190
},
163191
tabErrors() {
@@ -176,17 +204,10 @@ export default {
176204
};
177205
},
178206
defaultBackendPathRules() {
179-
const rulesExist = (this.value?.spec?.rules || []).length > 0;
180-
const defaultBackendExist = !!this.value?.spec?.defaultBackend?.service;
181-
182-
if (!rulesExist || defaultBackendExist) {
183-
return {
184-
name: this.fvGetAndReportPathRules('spec.defaultBackend.service.name'),
185-
port: this.fvGetAndReportPathRules('spec.defaultBackend.service.port'),
186-
};
187-
}
188-
189-
return { name: [], port: [] };
207+
return {
208+
name: this.fvGetAndReportPathRules('spec.defaultBackend.service.name'),
209+
port: this.fvGetAndReportPathRules('spec.defaultBackend.service.port'),
210+
};
190211
},
191212
serviceTargets() {
192213
return this.ingressHelper.findAndMapServiceTargets(this.services);

shell/utils/__tests__/ingress.test.ts

Lines changed: 127 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
import IngressDetailEditHelper from '@shell/utils/ingress';
22
import { SECRET_TYPES as TYPES } from '@shell/config/secret';
3+
import { get, set } from '@shell/utils/object';
34

4-
/**
5-
* Mirrors the port parsing logic used in RulePath.vue and DefaultBackend.vue
6-
* to decide whether to write port.number or port.name
7-
*/
85
function parseServicePort(rawPort: string | number): string | number {
96
const parsed = Number.parseInt(String(rawPort));
107

118
return Number.isNaN(parsed) ? rawPort : parsed;
129
}
1310

14-
/**
15-
* Mirrors the portRequired validation rule from index.vue.
16-
* Handles both scalar values (from component-level validation)
17-
* and port objects (from form-validation mixin).
18-
*/
1911
function portRequired(port: any): string | undefined {
2012
if (typeof port === 'string' || typeof port === 'number') {
2113
if (!port) {
@@ -28,10 +20,6 @@ function portRequired(port: any): string | undefined {
2820
return undefined;
2921
}
3022

31-
/**
32-
* Mirrors the portRange validation rule from index.vue.
33-
* Checks that a numeric port is within 1-65535 (handles both scalar and object inputs).
34-
*/
3523
function portRange(port: any): string | undefined {
3624
let num;
3725

@@ -50,6 +38,45 @@ function portRange(port: any): string | undefined {
5038
return undefined;
5139
}
5240

41+
/**
42+
* Mirrors willSave() from index.vue.
43+
* Clears the default backend when the service name or port is missing.
44+
*/
45+
function willSave(spec: any, paths: { defaultBackendPath: string; serviceNamePath: string; servicePortPath: string; servicePortNamePath: string }): void {
46+
const backend = get(spec, paths.defaultBackendPath);
47+
const serviceName = get(backend, paths.serviceNamePath);
48+
const servicePort = get(backend, paths.servicePortPath) ||
49+
get(backend, paths.servicePortNamePath);
50+
51+
if (backend && (!serviceName || !servicePort)) {
52+
set(spec, paths.defaultBackendPath, null);
53+
}
54+
}
55+
56+
/**
57+
* Mirrors defaultBackendNameRequired from index.vue.
58+
* Only enforces required when a service is actually selected.
59+
*/
60+
function defaultBackendNameRequired(name: any, hasService: boolean): string | undefined {
61+
if (hasService && !name) {
62+
return 'Target Service is required';
63+
}
64+
65+
return undefined;
66+
}
67+
68+
/**
69+
* Mirrors defaultBackendPortRequired from index.vue.
70+
* Delegates to portRequired only when a service is selected.
71+
*/
72+
function defaultBackendPortRequired(port: any, hasService: boolean): string | undefined {
73+
if (!hasService) {
74+
return undefined;
75+
}
76+
77+
return portRequired(port);
78+
}
79+
5380
const makeHelper = () => new IngressDetailEditHelper({
5481
$store: {} as any,
5582
namespace: 'default',
@@ -436,4 +463,91 @@ describe('ingress', () => {
436463
});
437464
});
438465
});
466+
467+
describe('willSave', () => {
468+
const nestedPaths = {
469+
defaultBackendPath: 'defaultBackend',
470+
serviceNamePath: 'service.name',
471+
servicePortPath: 'service.port.number',
472+
servicePortNamePath: 'service.port.name',
473+
};
474+
475+
it('preserves backend when port.number is set', () => {
476+
const spec = { defaultBackend: { service: { name: 'my-svc', port: { number: 80 } } } };
477+
478+
willSave(spec, nestedPaths);
479+
expect(spec.defaultBackend).toStrictEqual({ service: { name: 'my-svc', port: { number: 80 } } });
480+
});
481+
482+
it('preserves backend when port.name is set', () => {
483+
const spec = { defaultBackend: { service: { name: 'my-svc', port: { name: 'http' } } } };
484+
485+
willSave(spec, nestedPaths);
486+
expect(spec.defaultBackend).toStrictEqual({ service: { name: 'my-svc', port: { name: 'http' } } });
487+
});
488+
489+
it('clears backend when service name is empty', () => {
490+
const spec = { defaultBackend: { service: { name: '', port: { number: 80 } } } };
491+
492+
willSave(spec, nestedPaths);
493+
expect(spec.defaultBackend).toBeNull();
494+
});
495+
496+
it('clears backend when both port paths are empty', () => {
497+
const spec = { defaultBackend: { service: { name: 'my-svc', port: {} } } };
498+
499+
willSave(spec, nestedPaths);
500+
expect(spec.defaultBackend).toBeNull();
501+
});
502+
503+
it('clears backend when port is undefined', () => {
504+
const spec = { defaultBackend: { service: { name: 'my-svc' } } };
505+
506+
willSave(spec, nestedPaths);
507+
expect(spec.defaultBackend).toBeNull();
508+
});
509+
510+
it('does nothing when there is no backend', () => {
511+
const spec = { rules: [{}] } as any;
512+
513+
willSave(spec, nestedPaths);
514+
expect(spec).toStrictEqual({ rules: [{}] });
515+
});
516+
});
517+
518+
describe('defaultBackendNameRequired', () => {
519+
it('returns error when service is selected but name is empty', () => {
520+
expect(defaultBackendNameRequired('', true)).toBeDefined();
521+
});
522+
523+
it('passes when service is selected and name is set', () => {
524+
expect(defaultBackendNameRequired('my-svc', true)).toBeUndefined();
525+
});
526+
527+
it('passes when no service is selected and name is empty', () => {
528+
expect(defaultBackendNameRequired('', false)).toBeUndefined();
529+
});
530+
531+
it('passes when no service is selected and name is undefined', () => {
532+
expect(defaultBackendNameRequired(undefined, false)).toBeUndefined();
533+
});
534+
});
535+
536+
describe('defaultBackendPortRequired', () => {
537+
it('delegates to portRequired when service is selected', () => {
538+
expect(defaultBackendPortRequired('', true)).toBeDefined();
539+
expect(defaultBackendPortRequired(80, true)).toBeUndefined();
540+
expect(defaultBackendPortRequired('http', true)).toBeUndefined();
541+
expect(defaultBackendPortRequired({ number: 80 }, true)).toBeUndefined();
542+
expect(defaultBackendPortRequired({ name: 'http' }, true)).toBeUndefined();
543+
expect(defaultBackendPortRequired({}, true)).toBeDefined();
544+
});
545+
546+
it('skips validation when no service is selected', () => {
547+
expect(defaultBackendPortRequired('', false)).toBeUndefined();
548+
expect(defaultBackendPortRequired(undefined, false)).toBeUndefined();
549+
expect(defaultBackendPortRequired(null, false)).toBeUndefined();
550+
expect(defaultBackendPortRequired({}, false)).toBeUndefined();
551+
});
552+
});
439553
});

0 commit comments

Comments
 (0)