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' ;
912import { useStore } from ' vuex' ;
1013import { 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
3441interface 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+
183294function 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 >
0 commit comments