From 1c176f5384ff833ad5899d822dd242d460ba89af Mon Sep 17 00:00:00 2001 From: Jordon Leach Date: Tue, 20 Aug 2024 14:12:20 -0400 Subject: [PATCH 1/2] Support `matchConditions` for policy spec --- .../kubewarden/admission/MatchConditions.vue | 194 ++++++++++++++++++ .../chart/kubewarden/admission/index.vue | 9 +- pkg/kubewarden/components/Policies/Create.vue | 4 + pkg/kubewarden/l10n/en-us.yaml | 8 + pkg/kubewarden/types/kubewarden.ts | 4 +- pkg/kubewarden/types/policy.ts | 1 + .../charts/admission/MatchConditions.spec.ts | 75 +++++++ 7 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue create mode 100644 tests/unit/charts/admission/MatchConditions.spec.ts diff --git a/pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue b/pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue new file mode 100644 index 00000000..a0f960eb --- /dev/null +++ b/pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/pkg/kubewarden/chart/kubewarden/admission/index.vue b/pkg/kubewarden/chart/kubewarden/admission/index.vue index 40eee39c..38168bf0 100644 --- a/pkg/kubewarden/chart/kubewarden/admission/index.vue +++ b/pkg/kubewarden/chart/kubewarden/admission/index.vue @@ -16,6 +16,7 @@ import Rules from './Rules'; import NamespaceSelector from './NamespaceSelector'; import Settings from './Settings'; import ContextAware from './ContextAware'; +import MatchConditions from './MatchConditions'; export default { props: { @@ -34,7 +35,7 @@ export default { }, components: { - General, Questions, Rules, NamespaceSelector, Settings, ContextAware, Tab + General, Questions, Rules, NamespaceSelector, Settings, ContextAware, MatchConditions, Tab }, inject: ['chartType'], @@ -162,7 +163,11 @@ export default { - + + + + + diff --git a/pkg/kubewarden/components/Policies/Create.vue b/pkg/kubewarden/components/Policies/Create.vue index 06c04ed2..12106414 100644 --- a/pkg/kubewarden/components/Policies/Create.vue +++ b/pkg/kubewarden/components/Policies/Create.vue @@ -630,6 +630,10 @@ $color: var(--body-text) !important; } } +::v-deep .footer-error { + margin-top: 15px; +} + .wizard { position: relative; height: 100%; diff --git a/pkg/kubewarden/l10n/en-us.yaml b/pkg/kubewarden/l10n/en-us.yaml index dfeb53bf..b2f52b01 100644 --- a/pkg/kubewarden/l10n/en-us.yaml +++ b/pkg/kubewarden/l10n/en-us.yaml @@ -230,9 +230,17 @@ kubewarden: namespaceSelector: Namespace Selector settings: Settings contextAware: Context Aware Resources + matchConditions: Match Conditions serverSelect: label: Policy Server tooltip: The PolicyServer that will receive the requests to be validated. + matchConditions: + label: Match Conditions + add: Add Match Condition + remove: Remove Condition + description: Match Conditions use CEL expressions to define fine-grained request filtering for policies, evaluating conditions before applying policy rules. This field only takes effect if the Kubernetes cluster has the AdmissionWebhookMatchConditions feature gate enabled. + name: + placeholder: e.g. exclude-resource module: label: Module tooltip: This is the WebAssembly module that holds the validation or mutation logic. diff --git a/pkg/kubewarden/types/kubewarden.ts b/pkg/kubewarden/types/kubewarden.ts index 8dbebd22..3c79a18f 100644 --- a/pkg/kubewarden/types/kubewarden.ts +++ b/pkg/kubewarden/types/kubewarden.ts @@ -1,5 +1,6 @@ import { - V1SecurityContext, V1PodSecurityContext, V1ObjectMeta, V1EnvVar, V1LabelSelector, V1Condition + V1SecurityContext, V1PodSecurityContext, V1ObjectMeta, V1EnvVar, V1LabelSelector, V1Condition, + V1MatchCondition } from '@kubernetes/client-node'; export const KUBEWARDEN_PRODUCT_NAME = 'kubewarden'; @@ -84,6 +85,7 @@ export interface Policy { metadata: V1ObjectMeta; spec: { backgroundAudit?: boolean; + matchConditions?: V1MatchCondition[]; matchPolicy?: string; mode?: string; module: string; diff --git a/pkg/kubewarden/types/policy.ts b/pkg/kubewarden/types/policy.ts index 6507e174..8a4d0239 100644 --- a/pkg/kubewarden/types/policy.ts +++ b/pkg/kubewarden/types/policy.ts @@ -18,6 +18,7 @@ export const DEFAULT_POLICY: Policy = { resources: [], operations: [] }], + matchConditions: [], mutating: false, namespaceSelector: { matchExpressions: [], diff --git a/tests/unit/charts/admission/MatchConditions.spec.ts b/tests/unit/charts/admission/MatchConditions.spec.ts new file mode 100644 index 00000000..f7f537d1 --- /dev/null +++ b/tests/unit/charts/admission/MatchConditions.spec.ts @@ -0,0 +1,75 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { shallowMount } from '@vue/test-utils'; + +import MatchConditions from '@kubewarden/chart/kubewarden/admission/MatchConditions.vue'; + +jest.mock('@shell/components/CodeMirror', () => ({ template: '
' })); + +const createWrapper = (propsData = {}, mocks = {}) => { + return shallowMount(MatchConditions, { + propsData, + mocks, + stubs: { + CodeMirror: true, + InfoBox: true, + LabeledInput: true, + }, + }); +}; + +const testCondition1 = { name: 'exclude-leases', expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")' }; +const testCondition2 = { name: 'rbac', expression: 'request.resource.group != "rbac.authorization.k8s.io"' }; + +describe('MatchConditions.vue', () => { + it('renders the correct number of conditions based on the initial value', () => { + const value = { policy: { spec: { matchConditions: [testCondition1, testCondition2] } } }; + const wrapper = createWrapper({ value }); + + expect(wrapper.findAll('.condition').length).toBe(2); + }); + + it('adds a new condition when addCondition is called', async() => { + const value = { policy: { spec: { matchConditions: [] } } }; + const wrapper = createWrapper({ value }); + + wrapper.vm.addCondition(); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.matchConditions.length).toBe(1); + expect(wrapper.findAll('.condition').length).toBe(1); + }); + + it('removes a condition when removeCondition is called', async() => { + const value = { policy: { spec: { matchConditions: [testCondition1, testCondition2] } } }; + const wrapper = createWrapper({ value }); + + wrapper.vm.removeCondition(0); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.matchConditions.length).toBe(1); + expect(wrapper.findAll('.condition').length).toBe(1); + }); + + it('handles input correctly when handleInput is called', async() => { + const newExp = '!("system:nodes" in request.userInfo.groups)'; + const value = { policy: { spec: { matchConditions: [testCondition1, testCondition2] } } }; + const wrapper = createWrapper({ value }); + + wrapper.vm.handleInput(newExp, 0); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.matchConditions[0].expression).toBe(newExp); + }); + + it('refreshes CodeMirror instances when activeTab is set to matchConditions', async() => { + const value = { policy: { spec: { matchConditions: [testCondition1, testCondition2] } } }; + const wrapper = createWrapper({ value }); + + wrapper.vm.$refs['cm-0'] = [{ refresh: jest.fn() }]; + wrapper.setProps({ activeTab: 'matchConditions' }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.$refs['cm-0'][0].refresh).toHaveBeenCalled(); + }); +}); From f8dc610c7193439701c2b7b6689ac1cfc92cb2a3 Mon Sep 17 00:00:00 2001 From: Jordon Leach Date: Wed, 21 Aug 2024 10:47:25 -0400 Subject: [PATCH 2/2] Fix matchConditions on edit and after thrown error --- .../kubewarden/admission/MatchConditions.vue | 17 ++++++++- .../chart/kubewarden/admission/index.vue | 10 ++++- pkg/kubewarden/components/Policies/Create.vue | 37 +++++++++++++++---- ...policies.kubewarden.io.admissionpolicy.vue | 6 +++ ...s.kubewarden.io.clusteradmissionpolicy.vue | 7 ++++ pkg/kubewarden/l10n/en-us.yaml | 2 + 6 files changed, 69 insertions(+), 10 deletions(-) diff --git a/pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue b/pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue index a0f960eb..c3345dbf 100644 --- a/pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue +++ b/pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue @@ -97,16 +97,23 @@ export default { }, methods: { + emitUpdate() { + this.$emit('update:matchConditions', this.matchConditions); + }, + addCondition() { this.matchConditions.push({ name: '', expression: '' }); + this.emitUpdate(); }, removeCondition(index) { this.matchConditions.splice(index, 1); + this.emitUpdate(); }, handleInput(e, index) { this.$set(this.matchConditions[index], 'expression', e); + this.emitUpdate(); } } }; @@ -141,7 +148,7 @@ export default { -

Expression

+

{{ t('kubewarden.policyConfig.matchConditions.expression.label') }}

- diff --git a/pkg/kubewarden/chart/kubewarden/admission/index.vue b/pkg/kubewarden/chart/kubewarden/admission/index.vue index 38168bf0..97386326 100644 --- a/pkg/kubewarden/chart/kubewarden/admission/index.vue +++ b/pkg/kubewarden/chart/kubewarden/admission/index.vue @@ -109,6 +109,14 @@ export default { setActiveTab(tab) { this.activeTab = tab; + }, + + updateMatchConditions(matchConditions) { + if ( !this.chartValues.policy.spec ) { + this.$set(this.chartValues.policy, 'spec', {}); + } + + this.$set(this.chartValues.policy.spec, 'matchConditions', matchConditions); } } }; @@ -164,7 +172,7 @@ export default { - + diff --git a/pkg/kubewarden/components/Policies/Create.vue b/pkg/kubewarden/components/Policies/Create.vue index 12106414..6759b80d 100644 --- a/pkg/kubewarden/components/Policies/Create.vue +++ b/pkg/kubewarden/components/Policies/Create.vue @@ -61,8 +61,6 @@ export default ({ mixins: [CreateEditView], async fetch() { - this.errors = []; - if ( this.hasArtifactHub ) { await this.getPackages(); } @@ -73,7 +71,6 @@ export default ({ data() { return { - errors: [], bannerTitle: null, shortDescription: null, loadingPackages: false, @@ -91,6 +88,7 @@ export default ({ hasCustomPolicy: false, yamlOption: VALUES_STATE.FORM, + finishAttempts: 0, // Steps stepPolicies: { @@ -296,21 +294,46 @@ export default ({ } removeEmptyAttrs(out); // Clean up empty values from questions - merge(this.value, out); + + if ( this.finishAttempts > 0 ) { + // Remove keys that are not in the new spec + Object.keys(this.value.spec).forEach((key) => { + if ( !(key in out.spec) ) { + this.$delete(this.value.spec, key); + } + }); + + // Then, set or update the remaining keys + Object.keys(out.spec).forEach((key) => { + this.$set(this.value.spec, key, out.spec[key]); + }); + } else { + merge(this.value, out); + } // If create new namespace option is selected, create the ns before saving the policy if ( this.chartType === KUBEWARDEN.ADMISSION_POLICY && this.chartValues?.isNamespaceNew ) { await this.createNamespace(this.value?.metadata?.namespace); } - await this.save(event); + await this.attemptSave(event); } catch (e) { - handleGrowl({ error: e, store: this.$store }); - console.error('Error creating policy', e); // eslint-disable-line no-console } }, + async attemptSave(event) { + await this.save(event); + + // Check for errors set by the mixin + if ( this.errors && this.errors.length > 0 ) { + const error = new Error('Save operation failed'); + + this.finishAttempts++; + throw error; // Force an error to be caught in the finish method + } + }, + /** Fetch packages from ArtifactHub repository */ async getPackages() { this.repository = await this.value.artifactHubRepo(); diff --git a/pkg/kubewarden/edit/policies.kubewarden.io.admissionpolicy.vue b/pkg/kubewarden/edit/policies.kubewarden.io.admissionpolicy.vue index a2b95ee7..cf9d857b 100644 --- a/pkg/kubewarden/edit/policies.kubewarden.io.admissionpolicy.vue +++ b/pkg/kubewarden/edit/policies.kubewarden.io.admissionpolicy.vue @@ -84,3 +84,9 @@ export default { /> + + \ No newline at end of file diff --git a/pkg/kubewarden/edit/policies.kubewarden.io.clusteradmissionpolicy.vue b/pkg/kubewarden/edit/policies.kubewarden.io.clusteradmissionpolicy.vue index b2fa6b4f..bdcc5e73 100644 --- a/pkg/kubewarden/edit/policies.kubewarden.io.clusteradmissionpolicy.vue +++ b/pkg/kubewarden/edit/policies.kubewarden.io.clusteradmissionpolicy.vue @@ -82,6 +82,7 @@ export default { + + \ No newline at end of file diff --git a/pkg/kubewarden/l10n/en-us.yaml b/pkg/kubewarden/l10n/en-us.yaml index b2f52b01..164544b4 100644 --- a/pkg/kubewarden/l10n/en-us.yaml +++ b/pkg/kubewarden/l10n/en-us.yaml @@ -241,6 +241,8 @@ kubewarden: description: Match Conditions use CEL expressions to define fine-grained request filtering for policies, evaluating conditions before applying policy rules. This field only takes effect if the Kubernetes cluster has the AdmissionWebhookMatchConditions feature gate enabled. name: placeholder: e.g. exclude-resource + expression: + label: Expression module: label: Module tooltip: This is the WebAssembly module that holds the validation or mutation logic.