Skip to content

Commit

Permalink
Merge pull request #876 from jordojordo/match-conditions
Browse files Browse the repository at this point in the history
Support `matchConditions` for policy spec
  • Loading branch information
jordojordo authored Aug 23, 2024
2 parents cf62a2a + f8dc610 commit 5a3d7e6
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 10 deletions.
207 changes: 207 additions & 0 deletions pkg/kubewarden/chart/kubewarden/admission/MatchConditions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<script>
import { _CREATE, _VIEW } from '@shell/config/query-params';
import CodeMirror from '@shell/components/CodeMirror';
import InfoBox from '@shell/components/InfoBox';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
props: {
activeTab: {
type: String,
default: null
},
mode: {
type: String,
default: _CREATE
},
value: {
type: Object,
required: true
}
},
components: {
CodeMirror, InfoBox, LabeledInput
},
data() {
const matchConditions = this.value?.policy?.spec?.matchConditions || [];
return { matchConditions };
},
watch: {
activeTab() {
if ( this.activeTab === 'matchConditions' ) {
this.$nextTick(() => {
Object.keys(this.$refs).forEach((refKey) => {
if ( refKey.startsWith('cm-') ) {
const cmInstance = this.$refs[refKey][0];
if ( cmInstance && typeof cmInstance.refresh === 'function' ) {
cmInstance.refresh();
}
}
});
});
}
}
},
computed: {
isView() {
return this.mode === _VIEW;
},
codeMirrorOptions() {
const readOnly = this.isView;
const gutters = [];
if ( !readOnly ) {
gutters.push('CodeMirror-lint-markers');
}
gutters.push('CodeMirror-foldgutter');
return {
readOnly,
gutters,
mode: 'javascript',
lint: !readOnly,
lineNumbers: !readOnly,
styleActiveLine: true,
tabSize: 2,
indentWithTabs: false,
cursorBlinkRate: ( readOnly ? -1 : 530 ),
extraKeys: {
'Ctrl-Space': 'autocomplete',
Tab: (cm) => {
if ( cm.somethingSelected() ) {
cm.indentSelection('add');
return;
}
cm.execCommand('insertSoftTab');
},
'Shift-Tab': (cm) => {
cm.indentSelection('subtract');
}
}
};
}
},
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();
}
}
};
</script>
<template>
<div>
<p v-clean-html="t('kubewarden.policyConfig.matchConditions.description', {}, true)" class="mb-20" />
<div v-for="(condition, index) in matchConditions" :key="index" class="mb-20 condition">
<InfoBox>
<div class="condition__name-container">
<LabeledInput
v-model="condition.name"
class="mb-10 condition__name"
:data-testid="`kw-policy-match-condition-name-input-${ index }`"
:mode="mode"
:label="t('kubewarden.generic.name')"
:placeholder="t('kubewarden.policyConfig.matchConditions.name.placeholder')"
:required="true"
/>
<button
v-if="!isView"
:data-testid="`kw-policy-match-condition-remove-button-${ index }`"
type="button"
:disabled="isView"
class="btn role-link remove btn-sm"
@click="removeCondition(index)"
>
{{ t('kubewarden.policyConfig.matchConditions.remove') }}
</button>
</div>
<h4>{{ t('kubewarden.policyConfig.matchConditions.expression.label') }}</h4>
<CodeMirror
:ref="`cm-${ index }`"
:value="condition.expression"
:options="codeMirrorOptions"
:class="{fill: true}"
:data-testid="`kw-policy-match-condition-expression-${ index }`"
@onInput="(e) => handleInput(e, index)"
/>
</InfoBox>
</div>
<button
v-if="!isView"
data-testid="kw-policy-match-condition-add"
type="button"
class="btn role-tertiary add"
@click="addCondition"
>
{{ t('kubewarden.policyConfig.matchConditions.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
::v-deep code {
padding: 2px;
}
.fill {
flex: 1;
}
::v-deep .code-mirror {
position: relative;
.CodeMirror {
background-color: var(--yaml-editor-bg);
& .CodeMirror-gutters {
background-color: var(--yaml-editor-bg);
}
}
}
.condition {
&__name-container {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
&__name {
width: 50%;
}
}
</style>
17 changes: 15 additions & 2 deletions pkg/kubewarden/chart/kubewarden/admission/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -34,7 +35,7 @@ export default {
},
components: {
General, Questions, Rules, NamespaceSelector, Settings, ContextAware, Tab
General, Questions, Rules, NamespaceSelector, Settings, ContextAware, MatchConditions, Tab
},
inject: ['chartType'],
Expand Down Expand Up @@ -108,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);
}
}
};
Expand Down Expand Up @@ -162,7 +171,11 @@ export default {
</Tab>
</template>
<Tab name="rules" :label="t('kubewarden.policyConfig.tabs.rules')" :weight="95">
<Tab name="matchConditions" :label="t('kubewarden.policyConfig.tabs.matchConditions')" :weight="95" @active="setActiveTab('matchConditions')">
<MatchConditions v-model="chartValues" :active-tab="activeTab" :mode="mode" @update:matchConditions="updateMatchConditions" />
</Tab>
<Tab name="rules" :label="t('kubewarden.policyConfig.tabs.rules')" :weight="94">
<Rules v-model="chartValues" data-testid="kw-policy-config-rules-tab" :mode="mode" />
</Tab>
</div>
Expand Down
41 changes: 34 additions & 7 deletions pkg/kubewarden/components/Policies/Create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ export default ({
mixins: [CreateEditView],
async fetch() {
this.errors = [];
if ( this.hasArtifactHub ) {
await this.getPackages();
}
Expand All @@ -73,7 +71,6 @@ export default ({
data() {
return {
errors: [],
bannerTitle: null,
shortDescription: null,
loadingPackages: false,
Expand All @@ -91,6 +88,7 @@ export default ({
hasCustomPolicy: false,
yamlOption: VALUES_STATE.FORM,
finishAttempts: 0,
// Steps
stepPolicies: {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -630,6 +653,10 @@ $color: var(--body-text) !important;
}
}
::v-deep .footer-error {
margin-top: 15px;
}
.wizard {
position: relative;
height: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@ export default {
/>
</CruResource>
</template>

<style lang="scss" scoped>
::v-deep .cru__footer {
z-index: 1;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default {
<Create v-if="isCreate" :value="value" :mode="mode" />
<CruResource
v-else
:errors="errors"
:resource="value"
:mode="realMode"
:can-yaml="false"
Expand All @@ -94,3 +95,9 @@ export default {
/>
</CruResource>
</template>
<style lang="scss" scoped>
::v-deep .cru__footer {
z-index: 1;
}
</style>
10 changes: 10 additions & 0 deletions pkg/kubewarden/l10n/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,19 @@ 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 <a href="https://kubernetes.io/docs/reference/using-api/cel/" target="_blank" rel="noopener noreferrer nofollow">CEL expressions</a> 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 <code>AdmissionWebhookMatchConditions</code> 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.
Expand Down
Loading

0 comments on commit 5a3d7e6

Please sign in to comment.