dashboard/shared/gatekeeper-constraint.vue

321 lines
9.9 KiB
Vue

<script>
import Vue from 'vue';
import { merge } from 'lodash';
import jsyaml from 'js-yaml';
import { SCHEMA, NAMESPACE } from '@/config/types';
import MatchKinds from '@/components/form/MatchKinds';
import NameNsDescription from '@/components/form/NameNsDescription';
import CreateEditView from '@/mixins/create-edit-view';
import KeyValue from '@/components/form/KeyValue';
import LabeledSelect from '@/components/form/LabeledSelect';
import NamespaceList, { NAMESPACE_FILTERS } from '@/components/form/NamespaceList';
import Tab from '@/components/Tabbed/Tab';
import YamlEditor, { EDITOR_MODES } from '@/components/YamlEditor';
import Footer from '@/components/form/Footer';
import GatekeeperViolationsTable from '@/components/GatekeeperViolationsTable';
import RuleSelector from '@/components/form/RuleSelector';
import RadioGroup from '@/components/form/RadioGroup';
import { ucFirst } from '@/utils/string';
import { isSimpleKeyValue } from '@/utils/object';
import ResourceTabs from '@/components/form/ResourceTabs';
import { _VIEW, _CREATE } from '../config/query-params';
function findConstraintTypes(schemas) {
return schemas
.filter(schema => schema?.attributes?.group === 'constraints.gatekeeper.sh');
}
function findConstraintTypesIds(schemas) {
return findConstraintTypes(schemas)
.map(schema => schema.id);
}
const CONSTRAINT_PREFIX = 'constraints.gatekeeper.sh.';
const ENFORCEMENT_ACTION_VALUES = {
DENY: 'deny',
DRYRUN: 'dryrun'
};
export default {
components: {
Footer,
GatekeeperViolationsTable,
KeyValue,
LabeledSelect,
MatchKinds,
NameNsDescription,
NamespaceList,
RuleSelector,
RadioGroup,
Tab,
YamlEditor,
ResourceTabs
},
extends: CreateEditView,
props: {
value: {
type: Object,
required: true
}
},
data() {
return {
parametersYaml: this.value?.spec?.parameters ? jsyaml.safeDump(this.value.spec.parameters) : '',
showParametersAsYaml: !isSimpleKeyValue(this.value.spec.parameters),
enforcementActionOptions: Object.values(ENFORCEMENT_ACTION_VALUES),
enforcementActionLabels: Object.values(ENFORCEMENT_ACTION_VALUES).map(ucFirst),
NAMESPACE_FILTERS
};
},
computed: {
extraDetailColumns() {
return [
{
title: 'Template',
content: this.templateOptions.find(o => o.value === this.value.type).label
}
];
},
templateOptions() {
const schemas = this.$store.getters['cluster/all'](SCHEMA);
const constraintTypes = findConstraintTypesIds(schemas);
constraintTypes.sort();
return constraintTypes.map((type) => {
return {
label: type.replace(CONSTRAINT_PREFIX, ''),
value: type
};
});
},
isView() {
return this.mode === _VIEW;
},
isCreate() {
return this.mode === _CREATE;
},
editorMode() {
return this.mode === _VIEW
? EDITOR_MODES.VIEW_CODE
: EDITOR_MODES.EDIT_CODE;
},
canShowForm() {
return this.value?.spec?.parameters && isSimpleKeyValue(this.value.spec.parameters) && !this.isView;
},
systemNamespaceIds() {
return this.$store.getters['cluster/all'](NAMESPACE)
.filter(namespace => namespace.isSystem)
.map(namespace => namespace.id);
},
emptyDefaults() {
return {
type: this.templateOptions[0].value,
spec: {
enforcementAction: ENFORCEMENT_ACTION_VALUES.DENY,
parameters: {},
match: {
kinds: [{ apiGroups: [''] }],
namespaces: [],
excludedNamespaces: [],
labelSelector: { matchExpressions: [] },
namespaceSelector: { matchExpressions: [] }
}
}
};
},
isTemplateSelectorDisabled() {
return !this.isCreate;
}
},
watch: {
value: {
handler(value) {
// We have to set the type for the CreateEditView mixin to know what the type is when creating
this.type = value.type;
this.purgeNamespacesField(this.value);
},
deep: true
},
},
async created() {
if (!this.value.save) {
this.$emit('input', merge(await this.createConstraint(), this.value, this.emptyDefaults));
}
},
methods: {
async createConstraint() {
const constraint = await this.$store.dispatch('cluster/create', { type: this.templateOptions[0].value });
constraint.spec = { match: { excludedNamespaces: this.systemNamespaceIds } };
return constraint;
},
done() {
this.$router.replace({
name: 'c-cluster-gatekeeper-constraints',
params: this.$route.params
});
},
/**
* There's an upstream issue which prevents gatekeeper from processing namespaces with empty lists incorrectly.
* We need to remove the namespaces field if it's empty.
* https://github.com/open-policy-agent/gatekeeper/issues/508
*/
purgeNamespacesField(value) {
if (value?.spec?.match?.namespaces && (value.spec.match.namespaces.length === 0)) {
Vue.delete(value.spec.match, 'namespaces');
}
},
updateType(type) {
this.$set(this.value, 'type', type);
},
toggleParametersEditor() {
this.showParametersAsYaml = !this.showParametersAsYaml;
if (this.showParametersAsYaml) {
this.parametersYaml = jsyaml.safeDump(this.value.spec.parameters);
}
}
}
};
</script>
<template>
<div v-if="value.save" class="gatekeeper-constraint">
<div>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
:extra-columns="['template']"
:extra-detail-columns="extraDetailColumns"
>
<template v-slot:template>
<LabeledSelect
:mode="mode"
:value="value.type"
:options="templateOptions"
:disabled="isTemplateSelectorDisabled"
:label="t('gatekeeperConstraint.template')"
@input="updateType"
/>
</template>
</NameNsDescription>
</div>
<br />
<div v-if="isView">
<h2>{{ t('gatekeeperConstraint.violations.title') }}</h2>
<GatekeeperViolationsTable :constraint="value" />
<br />
</div>
<div>
<h2 class="parameters">
{{ t('gatekeeperConstraint.parameters.title') }}
<a v-if="showParametersAsYaml && canShowForm" href="#" @click="toggleParametersEditor">{{ t('gatekeeperConstraint.parameters.editAsForm') }}</a>
<a v-else-if="canShowForm" href="#" @click="toggleParametersEditor">{{ t('gatekeeperConstraint.parameters.editAsYaml') }}</a>
</h2>
<YamlEditor
v-if="showParametersAsYaml"
v-model="parametersYaml"
class="yaml-editor"
:editor-mode="editorMode"
@newObject="$set(value.spec, 'parameters', $event)"
/>
<KeyValue
v-else
v-model="value.spec.parameters"
:value-multiline="false"
:mode="mode"
:pad-left="false"
:read-allowed="false"
:as-map="true"
:add-label="t('gatekeeperConstraint.parameters.addParameter')"
:protip="false"
/>
</div>
<br />
<div>
<h2>{{ t('gatekeeperConstraint.enforcementAction.title') }}</h2>
<RadioGroup
v-model="value.spec.enforcementAction"
class="enforcement-action"
:options="enforcementActionOptions"
:labels="enforcementActionLabels"
:mode="mode"
@input="e=>value.spec.enforcementAction = e"
/>
</div>
<br />
<br />
<div class="match">
<h2>{{ t('gatekeeperConstraint.match.title') }}</h2>
<ResourceTabs v-model="value" :mode="mode" default-tab="labels">
<template #before>
<Tab name="namespaces" :label="t('gatekeeperConstraint.tab.namespaces.title')">
<div class="row">
<div class="col span-6">
<h4>{{ t('gatekeeperConstraint.tab.namespaces.sub.namespaces') }}</h4>
<NamespaceList v-model="value.spec.match.namespaces" :mode="mode" :namespace-filter="NAMESPACE_FILTERS.nonSystem" />
</div>
<div class="col span-6">
<h4>{{ t('gatekeeperConstraint.tab.namespaces.sub.excludedNamespaces') }}</h4>
<NamespaceList v-model="value.spec.match.excludedNamespaces" :mode="mode" />
</div>
</div>
</Tab>
<Tab name="selectors" :label="t('gatekeeperConstraint.tab.selectors.title')">
<div class="row">
<div class="col span-6">
<h4>{{ t('gatekeeperConstraint.tab.selectors.sub.labelSelector.title') }}</h4>
<RuleSelector
v-model="value.spec.match.labelSelector.matchExpressions"
:add-label="t('gatekeeperConstraint.tab.selectors.sub.labelSelector.addLabel')"
:mode="mode"
/>
</div>
<div class="col span-6">
<h4>{{ t('gatekeeperConstraint.tab.selectors.sub.namespaceSelector.title') }}</h4>
<RuleSelector
v-model="value.spec.match.namespaceSelector.matchExpressions"
:add-label="t('gatekeeperConstraint.tab.selectors.sub.namespaceSelector.addNamespace')"
:mode="mode"
/>
</div>
</div>
</Tab>
<Tab name="kinds" :label="t('gatekeeperConstraint.tab.kinds.title')">
<MatchKinds v-model="value.spec.match.kinds" :mode="mode" />
</Tab>
</template>
</ResourceTabs>
</div>
<Footer :mode="mode" :errors="errors" @save="save" @done="done" />
</div>
</template>
<style lang="scss">
.gatekeeper-constraint {
.yaml-editor {
margin-top: 10px;
height: 200px;
}
.parameters a {
font-size: 12px;
font-weight: 500;
}
.enforcement-action {
max-width: 200px;
}
}
</style>