Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 111 additions & 18 deletions pkg/kubewarden/chart/kubewarden/admission/General.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { RadioGroup } from '@components/Form/Radio';

import { KUBEWARDEN, KUBEWARDEN_APPS } from '@kubewarden/types';
import { buildModuleString } from '@kubewarden/modules/policyChart';
import { buildModuleString, parseModuleString } from '@kubewarden/modules/policyChart';

export default {
name: 'General',
Expand Down Expand Up @@ -104,22 +104,38 @@
moduleInfo: {
immediate: true,
handler(info) {
if (info) {
this.policyRegistry = info.registry ?? '';
this.policyRepository = info.repository ?? '';
this.policyTag = info.tag ?? '';
const module = this.policy?.spec?.module || '';

if (!module) {
// No module stored yet — seed fields from chart metadata defaults.
if (info) {
this.policyRegistry = info.registry ?? '';
this.policyRepository = info.repository ?? '';
this.policyTag = info.tag ?? '';
}

return;
}

// Module is set: always derive the three fields from spec.module,
// using moduleInfo.repository as an anchor to correctly identify the registry
// prefix even when it is not a valid hostname (e.g. user typed 'asdasd').
const fields = this._splitModuleWithContext(module, info);

this.policyRegistry = fields.registry;
this.policyRepository = fields.repository;
this.policyTag = fields.tag;
}
},

policyRegistry() {
this.syncModule();
},
policyRepository() {
this.syncModule();
},
policyTag() {
this.syncModule();
policyRegistry() { this.syncModule(); },

Check warning on line 131 in pkg/kubewarden/chart/kubewarden/admission/General.vue

View workflow job for this annotation

GitHub Actions / lint

Closing curly brace should be on the same line as opening curly brace or on the line after the previous block

Check warning on line 131 in pkg/kubewarden/chart/kubewarden/admission/General.vue

View workflow job for this annotation

GitHub Actions / lint

Statement inside of curly braces should be on next line
policyRepository() { this.syncModule(); },

Check warning on line 132 in pkg/kubewarden/chart/kubewarden/admission/General.vue

View workflow job for this annotation

GitHub Actions / lint

Closing curly brace should be on the same line as opening curly brace or on the line after the previous block

Check warning on line 132 in pkg/kubewarden/chart/kubewarden/admission/General.vue

View workflow job for this annotation

GitHub Actions / lint

Statement inside of curly braces should be on next line
policyTag() { this.syncModule(); },

Check warning on line 133 in pkg/kubewarden/chart/kubewarden/admission/General.vue

View workflow job for this annotation

GitHub Actions / lint

Closing curly brace should be on the same line as opening curly brace or on the line after the previous block

Check warning on line 133 in pkg/kubewarden/chart/kubewarden/admission/General.vue

View workflow job for this annotation

GitHub Actions / lint

Statement inside of curly braces should be on next line

hasValuesModule(neu) {
if (!neu) {
this.$emit('module-validation', false);
}
},
},

Expand Down Expand Up @@ -203,20 +219,94 @@
},

methods: {
/**
* Splits an OCI module string into registry/repository/tag using moduleInfo.repository
* as an anchor so that non-hostname registry values (e.g. 'asdasd') are preserved
* correctly across YAML editor round-trips.
*/
_splitModuleWithContext(module, info) {
if (!module) {
return {
registry: '',
repository: '',
tag: ''
};
}

// 1. Use known repository path as an anchor to extract whatever precedes it as registry.
// This runs before the standard parse so that non-hostname prefixes like 'asdasd'
// are preserved in the registry field rather than absorbed into repository.
if (info?.repository) {
const withPrefix = `/${ info.repository }:`;
const prefixIdx = module.indexOf(withPrefix);

if (prefixIdx > 0) {
// There is a non-empty registry prefix before the known repository path.
return {
registry: module.slice(0, prefixIdx),
repository: info.repository,
tag: module.slice(prefixIdx + withPrefix.length),
};
}

const noPrefix = `${ info.repository }:`;

if (module.startsWith(noPrefix)) {
// No registry prefix — the module is repository:tag directly.
return {
registry: '',
repository: info.repository,
tag: module.slice(noPrefix.length),
};
}
}

// 2. Standard parse handles valid hostname registries (ghcr.io, docker.io, …)
const parsed = parseModuleString(module);

if (parsed) {
return parsed;
}

// 3. Last-resort: treat last colon as tag separator, first slash as registry/repo split.
const colonIdx = module.lastIndexOf(':');
const tag = colonIdx > 0 ? module.slice(colonIdx + 1) : '';
const rest = colonIdx > 0 ? module.slice(0, colonIdx) : module;
const slashIdx = rest.indexOf('/');

if (slashIdx > 0) {
return {
registry: rest.slice(0, slashIdx),
repository: rest.slice(slashIdx + 1),
tag,
};
}

return {
registry: '',
repository: rest,
tag,
};
},

syncModule() {
if (!this.hasValuesModule || !this.policy?.spec) {
if (!this.policy?.spec) {
return;
}

const registry = this.policyRegistry?.trim() || '';
const repository = this.policyRepository?.trim() || '';
const tag = this.policyTag?.trim() || '';
if (!this.hasValuesModule) {
this.$emit('module-validation', false);

if (!repository || !tag) {
return;
}

const registry = this.policyRegistry?.trim() || '';
const repository = this.policyRepository?.trim() || '';
const tag = this.policyTag?.trim() || '';

this.policy.spec.module = buildModuleString(registry, repository, tag);

this.$emit('module-validation', !registry || !repository || !tag);
}
}
};
Expand Down Expand Up @@ -274,6 +364,7 @@
:mode="mode"
:label="t('kubewarden.policies.module.registry')"
:placeholder="t('kubewarden.policies.module.registryPlaceholder')"
:required="true"
/>
</div>
<div class="col span-5">
Expand All @@ -282,6 +373,7 @@
data-testid="kw-policy-general-repository-input"
:mode="mode"
:label="t('kubewarden.policies.module.repository')"
:required="true"
/>
</div>
<div class="col span-3">
Expand All @@ -290,6 +382,7 @@
data-testid="kw-policy-general-tag-input"
:mode="mode"
:label="t('kubewarden.policies.module.tag')"
:required="true"
/>
</div>
</template>
Expand Down
168 changes: 168 additions & 0 deletions pkg/kubewarden/chart/kubewarden/admission/__tests__/General.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,172 @@ describe('component: General', () => {

expect(wrapper.vm.policy.spec.module).toBe('registry.internal:5000/kubewarden/pod-privileged:v2.0.0');
});

it('in view mode splits spec.module into 3 fields', () => {
const wrapper = shallowMount(General, {
props: {
targetNamespace: 'default',
value: {
policy: {
...userGroupPolicy,
spec: {
...userGroupPolicy.spec,
module: 'ghcr.io/kubewarden/pod-privileged:v1.2.3'
}
}
},
moduleInfo: {
registry: 'ghcr.io',
repository: 'kubewarden/pod-privileged',
tag: 'v1.2.3',
source: 'values'
}
},
global: {
provide: { chartType: KUBEWARDEN.CLUSTER_ADMISSION_POLICY },
mocks: {
$fetchState: { pending: false },
$store: {
getters: {
currentStore: () => 'current_store',
'current_store/all': jest.fn(),
'i18n/t': jest.fn()
},
}
},
stubs: {
NameNsDescription: { template: '<span />' },
RadioGroup: { template: '<span />' },
LabeledTooltip: { template: '<span />' },
LabeledInput: { template: '<span />' }
}
},
computed: {
isCreate: () => false,
policyServers: () => [],
policyServerOptions: () => [],
isGlobal: () => true,
hasValuesModule: () => true,
showModeBanner: () => false,
modeDisabled: () => false
}
});

expect(wrapper.vm.policyRegistry).toBe('ghcr.io');
expect(wrapper.vm.policyRepository).toBe('kubewarden/pod-privileged');
expect(wrapper.vm.policyTag).toBe('v1.2.3');
});

it('after user clears registry, remount keeps it empty (not restored from moduleInfo)', () => {
// Simulates: user clears registry → syncModule writes repo:tag → YAML toggle → remount
const wrapper = shallowMount(General, {
props: {
targetNamespace: 'default',
value: {
policy: {
...userGroupPolicy,
spec: {
...userGroupPolicy.spec,
// spec.module was rebuilt by syncModule after user cleared registry
module: 'kubewarden/pod-privileged:v1.2.3'
}
}
},
moduleInfo: {
registry: 'ghcr.io',
repository: 'kubewarden/pod-privileged',
tag: 'v1.2.3',
source: 'values'
}
},
global: {
provide: { chartType: KUBEWARDEN.CLUSTER_ADMISSION_POLICY },
mocks: {
$fetchState: { pending: false },
$store: {
getters: {
currentStore: () => 'current_store',
'current_store/all': jest.fn(),
'i18n/t': jest.fn()
},
}
},
stubs: {
NameNsDescription: { template: '<span />' },
RadioGroup: { template: '<span />' },
LabeledTooltip: { template: '<span />' },
LabeledInput: { template: '<span />' }
}
},
computed: {
isCreate: () => false,
policyServers: () => [],
policyServerOptions: () => [],
isGlobal: () => true,
hasValuesModule: () => true,
showModeBanner: () => false,
modeDisabled: () => false
}
});

expect(wrapper.vm.policyRegistry).toBe('');
expect(wrapper.vm.policyRepository).toBe('kubewarden/pod-privileged');
expect(wrapper.vm.policyTag).toBe('v1.2.3');
});

it('after user types non-hostname registry, remount preserves it in registry (not moved to repository)', () => {
// Simulates: user types 'asdasd' → syncModule writes asdasd/repo:tag → YAML toggle → remount
const wrapper = shallowMount(General, {
props: {
targetNamespace: 'default',
value: {
policy: {
...userGroupPolicy,
spec: {
...userGroupPolicy.spec,
module: 'asdasd/kubewarden/pod-privileged:v1.2.3'
}
}
},
moduleInfo: {
registry: 'ghcr.io',
repository: 'kubewarden/pod-privileged',
tag: 'v1.2.3',
source: 'values'
}
},
global: {
provide: { chartType: KUBEWARDEN.CLUSTER_ADMISSION_POLICY },
mocks: {
$fetchState: { pending: false },
$store: {
getters: {
currentStore: () => 'current_store',
'current_store/all': jest.fn(),
'i18n/t': jest.fn()
},
}
},
stubs: {
NameNsDescription: { template: '<span />' },
RadioGroup: { template: '<span />' },
LabeledTooltip: { template: '<span />' },
LabeledInput: { template: '<span />' }
}
},
computed: {
isCreate: () => false,
policyServers: () => [],
policyServerOptions: () => [],
isGlobal: () => true,
hasValuesModule: () => true,
showModeBanner: () => false,
modeDisabled: () => false
}
});

expect(wrapper.vm.policyRegistry).toBe('asdasd');
expect(wrapper.vm.policyRepository).toBe('kubewarden/pod-privileged');
expect(wrapper.vm.policyTag).toBe('v1.2.3');
});
});
13 changes: 10 additions & 3 deletions pkg/kubewarden/chart/kubewarden/admission/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ export default {

data() {
return {
activeTab: null,
chartValues: null
activeTab: null,
chartValues: null,
moduleFieldsMissing: false
};
},

Expand All @@ -82,7 +83,7 @@ export default {
},

generalTabError() {
return !this.chartValues?.policy?.metadata?.name || !this.chartValues?.policy?.spec?.module;
return !this.chartValues?.policy?.metadata?.name || !this.chartValues?.policy?.spec?.module || this.moduleFieldsMissing;
},

rulesTabError() {
Expand Down Expand Up @@ -161,6 +162,11 @@ export default {
},

methods: {
onModuleValidation(missing) {
this.moduleFieldsMissing = missing;
this.$emit('module-validation', missing);
},

settingsChanged(event) {
this.chartValues.policy.spec.settings = jsyaml.load(event);
},
Expand Down Expand Up @@ -190,6 +196,7 @@ export default {
:target-namespace="targetNamespace"
:is-custom="isCustom"
:module-info="moduleInfo"
@module-validation="onModuleValidation"
/>
</Tab>

Expand Down
Loading
Loading