Skip to content

Commit 3d99959

Browse files
committed
Add fix for Admission Policies
1 parent 5213dc7 commit 3d99959

5 files changed

Lines changed: 173 additions & 32 deletions

File tree

pkg/kubewarden/chart/kubewarden/admission/General.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ export default {
7070
}
7171
7272
// fix for https://github.com/rancher/kubewarden-ui/issues/672
73-
// enforce `default` as namespace for creation of AP's
74-
if (this.mode === _CREATE && this.chartType === KUBEWARDEN.ADMISSION_POLICY) {
73+
// default namespace for AP create only when not already preselected
74+
if (
75+
this.mode === _CREATE &&
76+
this.chartType === KUBEWARDEN.ADMISSION_POLICY &&
77+
!policy?.metadata?.namespace
78+
) {
7579
set(policy.metadata, 'namespace', 'default');
7680
}
7781

pkg/kubewarden/components/Policies/Values.vue

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
computed,
77
markRaw
88
} from 'vue';
9+
import cloneDeep from 'lodash/cloneDeep';
10+
import isEqual from 'lodash/isEqual';
11+
import jsyaml from 'js-yaml';
912
import { useStore } from 'vuex';
1013
import { defineAsyncComponent, toRaw } from 'vue';
1114
@@ -30,6 +33,10 @@ import {
3033
shouldShowBackToFormModal,
3134
useYamlCompareState
3235
} from '@kubewarden/composables/useYamlCompare';
36+
import {
37+
resolveQuestionDefault,
38+
shouldApplyQuestionDefault
39+
} from '@kubewarden/modules/policyDefaults';
3340
3441
interface Props {
3542
mode: string;
@@ -111,6 +118,9 @@ watch(yamlOption, (neu, old) => {
111118
112119
case VALUES_STATE.YAML:
113120
case VALUES_STATE.DIFF:
121+
// Past this point, treat changes as user intent only.
122+
isBootstrappingDefaults.value = false;
123+
114124
// Entering YAML/Compare from form takes a fresh snapshot.
115125
if (old === VALUES_STATE.FORM || !old) {
116126
currentYamlValues.value = getCurrentYamlState(
@@ -174,12 +184,113 @@ function syncMountDefaultsIntoBaseline(nextFormYaml: string) {
174184
return;
175185
}
176186
187+
if (!isKnownMountDefaultDelta(baselineYaml, nextFormYaml)) {
188+
// Stop bootstrap syncing once non-default mutations appear.
189+
isBootstrappingDefaults.value = false;
190+
191+
return;
192+
}
193+
177194
// Keep compare baseline aligned with mount-time form defaults so only user changes appear in diff.
178195
originalYamlValues.value = nextFormYaml;
179196
currentYamlValues.value = nextFormYaml;
180197
previousYamlValues.value = nextFormYaml;
181198
}
182199
200+
function buildQuestionDefaults() {
201+
const defaults: Record<string, any> = {};
202+
const questions = props.chartValues?.questions?.questions;
203+
204+
if (!Array.isArray(questions)) {
205+
return defaults;
206+
}
207+
208+
const setDeep = (obj: Record<string, any>, path: string, value: any) => {
209+
const keys = path.split('.');
210+
let current = obj;
211+
212+
for (let i = 0; i < keys.length - 1; i++) {
213+
const key = keys[i];
214+
215+
if (!current[key] || typeof current[key] !== 'object') {
216+
current[key] = {};
217+
}
218+
219+
current = current[key];
220+
}
221+
222+
current[keys[keys.length - 1]] = value;
223+
};
224+
225+
for (const q of questions) {
226+
const variable = q?.variable;
227+
const defaultValue = resolveQuestionDefault(q);
228+
229+
if (!variable) {
230+
continue;
231+
}
232+
233+
if (!shouldApplyQuestionDefault(undefined, defaultValue)) {
234+
continue;
235+
}
236+
237+
setDeep(defaults, variable, defaultValue);
238+
}
239+
240+
return defaults;
241+
}
242+
243+
function isKnownMountDefaultDelta(baselineYaml: string, nextFormYaml: string) {
244+
let baselineObj: Record<string, any>;
245+
let nextObj: Record<string, any>;
246+
247+
try {
248+
baselineObj = (jsyaml.load(baselineYaml) || {}) as Record<string, any>;
249+
nextObj = (jsyaml.load(nextFormYaml) || {}) as Record<string, any>;
250+
} catch {
251+
return false;
252+
}
253+
254+
const questionDefaults = buildQuestionDefaults();
255+
256+
const normalize = (obj: Record<string, any>) => {
257+
const out = cloneDeep(obj || {});
258+
const ns = out?.metadata?.namespace;
259+
260+
// Treat admission create defaults ('', undefined, and 'default') as equivalent.
261+
if (ns === 'default' || ns === '' || ns === undefined || ns === null) {
262+
out.metadata = out.metadata || {};
263+
out.metadata.namespace = '';
264+
}
265+
266+
// Treat default question-derived settings as mount-time defaults.
267+
const settings = out?.spec?.settings;
268+
269+
if (isEqual(settings || {}, questionDefaults)) {
270+
out.spec = out.spec || {};
271+
out.spec.settings = {};
272+
}
273+
274+
// Treat the default ClusterAdmissionPolicy namespace selector as mount-time default.
275+
const matchExpressions = out?.spec?.namespaceSelector?.matchExpressions;
276+
277+
if (
278+
Array.isArray(matchExpressions) &&
279+
(matchExpressions.length === 0 || isEqual(matchExpressions, [RANCHER_NS_MATCH_EXPRESSION]))
280+
) {
281+
delete out.spec.namespaceSelector.matchExpressions;
282+
283+
if (Object.keys(out.spec.namespaceSelector).length === 0) {
284+
delete out.spec.namespaceSelector;
285+
}
286+
}
287+
288+
return out;
289+
};
290+
291+
return isEqual(normalize(baselineObj), normalize(nextObj));
292+
}
293+
183294
function loadValuesComponent() {
184295
if (props.value?.haveComponent('kubewarden/admission')) {
185296
// Dynamic import of the form component
@@ -236,7 +347,6 @@ onMounted(() => {
236347
];
237348
}
238349
239-
isBootstrappingDefaults.value = false;
240350
fetchPending.value = false;
241351
});
242352
</script>

pkg/kubewarden/components/Policies/__tests__/Create.spec.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import Create from '@kubewarden/components/Policies/Create.vue';
66

77
// A minimal VersionInfo-like object whose parsePolicyModule() returns known values
88
const mockVersionInfo = {
9-
chart: { annotations: { 'kubewarden/registry': 'ghcr.io', 'kubewarden/repository': 'kubewarden/policies/cap-hostname', 'kubewarden/tag': 'v1.0.0' } },
9+
chart: {
10+
annotations: {
11+
'kubewarden/registry': 'ghcr.io',
12+
'kubewarden/repository': 'kubewarden/policies/cap-hostname',
13+
'kubewarden/tag': 'v1.0.0'
14+
}
15+
},
1016
values: {
1117
module: {
1218
repository: 'kubewarden/policies/cap-hostname',
@@ -16,18 +22,18 @@ const mockVersionInfo = {
1622
};
1723

1824
const mockStoreGetters = {
19-
currentStore: () => 'cluster',
20-
'cluster/schemaFor': () => null,
21-
'cluster/canList': () => false,
22-
'type-map/isSpoofed': () => false,
23-
'catalog/repos': [],
24-
'catalog/charts': [],
25-
'management/byId': () => null,
26-
'kubewarden/airGapped': false,
25+
currentStore: () => 'cluster',
26+
'cluster/schemaFor': () => null,
27+
'cluster/canList': () => false,
28+
'type-map/isSpoofed': () => false,
29+
'catalog/repos': [],
30+
'catalog/charts': [],
31+
'management/byId': () => null,
32+
'kubewarden/airGapped': false,
2733
'kubewarden/hideBannerOfficialRepo': true,
2834
'kubewarden/hideBannerPolicyRepo': true,
2935
'kubewarden/hideBannerAirgapPolicy': true,
30-
'i18n/t': (key: string) => key,
36+
'i18n/t': (key: string) => key,
3137
};
3238

3339
const mockStore = {
@@ -45,12 +51,19 @@ function createWrapper(overrides: any = {}) {
4551
mode: 'create',
4652
},
4753
global: {
48-
provide: { chartType: KUBEWARDEN.CLUSTER_ADMISSION_POLICY, realMode: 'create' },
54+
provide: {
55+
chartType: KUBEWARDEN.CLUSTER_ADMISSION_POLICY,
56+
realMode: 'create'
57+
},
4958
mocks: {
5059
$store: mockStore,
5160
$fetchState: { pending: false },
52-
$route: { params: {}, query: {}, hash: '' },
53-
$router: { replace: jest.fn() },
61+
$route: {
62+
params: {},
63+
query: {},
64+
hash: ''
65+
},
66+
$router: { replace: jest.fn() },
5467
},
5568
stubs: {
5669
Loading: { template: '<span />' },
@@ -153,7 +166,12 @@ describe('component: Create', () => {
153166
const wrapper = createWrapper();
154167

155168
(wrapper.vm as any).selectedPolicyDetails = mockVersionInfo;
156-
(wrapper.vm as any).policyModuleInfo = { registry: 'original', repository: 'repo', tag: 'tag', source: 'values' };
169+
(wrapper.vm as any).policyModuleInfo = {
170+
registry: 'original',
171+
repository: 'repo',
172+
tag: 'tag',
173+
source: 'values'
174+
};
157175
(wrapper.vm as any).chartValues = { policy: { spec: { module: 'repo:tag' } } };
158176
(wrapper.vm as any).yamlValues = 'spec:\n module: other/repo:newtag\n';
159177

@@ -169,7 +187,12 @@ describe('component: Create', () => {
169187

170188
(wrapper.vm as any).selectedPolicyDetails = mockVersionInfo;
171189
(wrapper.vm as any).selectedPolicyChart = 'custom'; // makes customPolicy computed = true
172-
(wrapper.vm as any).policyModuleInfo = { registry: 'original', repository: 'repo', tag: 'tag', source: 'values' };
190+
(wrapper.vm as any).policyModuleInfo = {
191+
registry: 'original',
192+
repository: 'repo',
193+
tag: 'tag',
194+
source: 'values'
195+
};
173196
(wrapper.vm as any).chartValues = { policy: { spec: { module: 'repo:tag' } } };
174197
(wrapper.vm as any).yamlValues = 'spec:\n module: 11111/22222:333333\n';
175198

pkg/kubewarden/components/PolicyServer/Values.vue

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
import { defineAsyncComponent, markRaw, toRaw } from 'vue';
33
import merge from 'lodash/merge';
4+
import cloneDeep from 'lodash/cloneDeep';
45
import isEqual from 'lodash/isEqual';
56
import isEmpty from 'lodash/isEmpty';
67
import jsyaml from 'js-yaml';
@@ -72,20 +73,20 @@ export default {
7273
return {
7374
VALUES_STATE,
7475
YAML_OPTIONS,
75-
currentYamlValues: '',
76-
originalYamlValues: '',
77-
previousYamlValues: '',
78-
formYamlValues: '',
79-
showForm: true,
80-
configValues: null,
81-
showQuestions: true,
82-
showValuesComponent: false,
83-
valuesComponent: null,
84-
preYamlOption: VALUES_STATE.FORM,
85-
yamlOption: VALUES_STATE.FORM,
76+
currentYamlValues: '',
77+
originalYamlValues: '',
78+
previousYamlValues: '',
79+
formYamlValues: '',
80+
showForm: true,
81+
configValues: null,
82+
showQuestions: true,
83+
showValuesComponent: false,
84+
valuesComponent: null,
85+
preYamlOption: VALUES_STATE.FORM,
86+
yamlOption: VALUES_STATE.FORM,
8687
yamlSnapshotsInitialized: false,
8788
isBootstrappingDefaults: true,
88-
values: this.chartValues
89+
values: this.chartValues
8990
};
9091
},
9192
@@ -197,12 +198,12 @@ export default {
197198
try {
198199
baselineObj = jsyaml.load(baselineYaml) || {};
199200
nextObj = jsyaml.load(nextFormYaml) || {};
200-
} catch (e) {
201+
} catch {
201202
return false;
202203
}
203204
204205
const normalize = (obj) => {
205-
const out = structuredClone(obj || {});
206+
const out = cloneDeep(obj || {});
206207
207208
if (out?.spec) {
208209
delete out.spec.image;

pkg/kubewarden/components/PolicyServer/__tests__/Values.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import Values from '@kubewarden/components/PolicyServer/Values.vue';
66
jest.mock('@shell/utils/create-yaml', () => ({
77
createYaml: (_schemas: any, _type: string, values: any) => {
88
return `image:\n repository: ${ values?.image?.repository || '' }\n tag: ${ values?.image?.tag || '' }\n`;
9+
},
10+
saferDump: (values: any) => {
11+
return `image:\n repository: ${ values?.image?.repository || '' }\n tag: ${ values?.image?.tag || '' }\n`;
912
}
1013
}));
1114

0 commit comments

Comments
 (0)