dashboard/shell/edit/constraints.gatekeeper.sh.c.../index.vue

378 lines
11 KiB
Vue

<script>
import merge from 'lodash/merge';
import { ucFirst } from '@shell/utils/string';
import { isSimpleKeyValue, set } from '@shell/utils/object';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import { SCHEMA, NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import { RadioGroup } from '@components/Form/Radio';
import RuleSelector from '@shell/components/form/RuleSelector';
import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed';
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
import CruResource from '@shell/components/CruResource';
import { ENFORCEMENT_ACTION_VALUES } from '@shell/models/constraints.gatekeeper.sh.constraint';
import { saferDump } from '@shell/utils/create-yaml';
import NamespaceList, { NAMESPACE_FILTERS_HELPER } from './NamespaceList';
import MatchKinds from './MatchKinds';
import Scope, { SCOPE_OPTIONS } from './Scope';
import Banner from '@components/Banner/Banner';
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.';
export default {
emits: ['update:value', 'set-subtype'],
components: {
CruResource,
MatchKinds,
NameNsDescription,
NamespaceList,
RuleSelector,
RadioGroup,
Scope,
Tab,
Tabbed,
YamlEditor,
Banner
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true
}
},
data() {
const emptySpec = {
enforcementAction: ENFORCEMENT_ACTION_VALUES.DENY,
parameters: {},
match: {
kinds: [{}],
namespaces: [],
excludedNamespaces: [],
labelSelector: { matchExpressions: [] },
namespaceSelector: { matchExpressions: [] }
}
};
if (this.mode === _CREATE) {
this.value['spec'] = merge(this.value.spec, emptySpec);
if (!this.value.spec.match.scope) {
this.value.spec.match['scope'] = SCOPE_OPTIONS[0].value;
}
} else {
this.value.spec['match'] = this.value.spec.match || {};
this.value.spec.match['kinds'] = this.value.spec.match.kinds || [{}];
this.value.spec.match['labelSelector'] = this.value.spec.match.labelSelector || {};
this.value.spec.match.labelSelector['matchExpressions'] = this.value.spec.match.labelSelector.matchExpressions || [];
this.value.spec.match['namespaceSelector'] = this.value.spec.match.namespaceSelector || {};
this.value.spec.match.namespaceSelector['labelSelector'] = this.value.spec.match.namespaceSelector.labelSelector || [];
}
const parametersYaml = saferDump(this.value?.spec?.parameters);
return {
emptySpec,
parametersYaml,
enforcementActionOptions: [
{
label: `${ ENFORCEMENT_ACTION_VALUES.DENY } - deny admission requests with any violation`,
value: ENFORCEMENT_ACTION_VALUES.DENY
},
{
label: `${ ENFORCEMENT_ACTION_VALUES.DRYRUN } - enables constraints to be deployed in the cluster without making actual changes`,
value: ENFORCEMENT_ACTION_VALUES.DRYRUN
}
],
enforcementActionLabels: Object.values(ENFORCEMENT_ACTION_VALUES).map(ucFirst),
NAMESPACE_FILTERS_HELPER,
};
},
computed: {
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
};
});
},
templateSubtypes() {
const schemas = this.$store.getters['cluster/all'](SCHEMA);
const constraintTypes = findConstraintTypes(schemas);
constraintTypes.sort();
return constraintTypes.map((type) => {
const splitId = type.id.split('.');
return {
label: type.id.replace(CONSTRAINT_PREFIX, ''),
description: '',
id: splitId[splitId.length - 1],
bannerAbbrv: 'CT'
};
});
},
editorMode() {
if ( this.mode === _VIEW ) {
return EDITOR_MODES.VIEW_CODE;
} else {
return 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: this.emptySpec
};
},
isTemplateSelectorDisabled() {
return !this.isCreate;
},
showNamespaceLists() {
return this.value.spec.match.scope !== 'Cluster';
}
},
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
},
},
created() {
this.registerBeforeHook(this.willSave, 'willSave');
if (!this.value.save) {
this.$emit('update:value', merge(this.value, this.emptyDefaults));
}
},
methods: {
willSave() {
this.value.spec.match.kinds.forEach((kind) => {
const apiGroups = kind.apiGroups || [];
if (apiGroups.length === 0) {
kind.apiGroups = ['*'];
}
});
},
/**
* 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)) {
delete value.spec.match['namespaces'];
}
},
updateType(type) {
this.value['type'] = type;
},
onTabChanged({ tab }) {
// This is necessary to force the yamlEditor to adjust the size once it has space to fill.
if (tab.name === 'parameters' && this.$refs.yamlEditor?.refresh) {
this.$refs.yamlEditor.refresh();
}
},
selectTemplateSubtype(subType) {
this.value['kind'] = subType;
this.$emit('set-subtype', subType);
},
onScopeChange(newScope) {
if (newScope === 'Cluster') {
this.value.spec.match.namespaces = [];
this.value.spec.match.excludedNamespaces = [];
}
},
setParameters(e) {
return set(this.value.spec, 'parameters', e);
}
}
};
</script>
<template>
<Banner
v-if="templateSubtypes.length === 0"
color="warning"
>
{{ t('gatekeeperConstraint.templateRequired') }}
</Banner>
<CruResource
v-else
:done-route="doneRoute"
:mode="mode"
:resource="value"
:subtypes="templateSubtypes"
:validation-passed="true"
:errors="errors"
@error="e=>errors = e"
@finish="save"
@select-type="selectTemplateSubtype"
>
<div
v-if="value"
class="gatekeeper-constraint"
>
<div>
<NameNsDescription
v-if="!isView"
:value="value"
:mode="mode"
:namespaced="false"
/>
</div>
<div class="row mb-40">
<div class="col span-12">
<h3>Enforcement Action</h3>
<RadioGroup
v-model:value="value.spec.enforcementAction"
name="enforcementAction"
class="enforcement-action"
:options="enforcementActionOptions"
:labels="enforcementActionLabels"
:mode="mode"
/>
</div>
</div>
<Tabbed
:side-tabs="true"
:use-hash="useTabbedHash"
@changed="onTabChanged"
>
<Tab
name="namespaces"
:label="t('gatekeeperConstraint.tab.namespaces.title')"
:weight="3"
>
<div class="row">
<div class="col span-6">
<Scope
v-model:value="value.spec.match.scope"
:mode="mode"
@update:value="onScopeChange($event)"
/>
</div>
</div>
<div
v-if="showNamespaceLists"
class="row mt-20"
>
<div class="col span-12">
<NamespaceList
v-model:value="value.spec.match.namespaces"
:label="t('gatekeeperConstraint.tab.namespaces.sub.namespaces')"
tooltip="If defined, a constraint will only apply to resources in a listed namespace."
:mode="mode"
:namespace-filter="NAMESPACE_FILTERS_HELPER.nonSystem"
add-label="Add Namespace"
/>
</div>
</div>
<div
v-if="showNamespaceLists"
class="row mt-20"
>
<div class="col span-12">
<NamespaceList
v-model:value="value.spec.match.excludedNamespaces"
:label="t('gatekeeperConstraint.tab.namespaces.sub.excludedNamespaces')"
tooltip="If defined, a constraint will only apply to resources not in a listed namespace."
:mode="mode"
add-label="Add Excluded Namespace"
/>
</div>
</div>
<div class="row mt-40">
<div class="col span-12">
<h3>{{ t('gatekeeperConstraint.tab.namespaces.sub.namespaceSelector.title') }}</h3>
<RuleSelector
v-model:value="value.spec.match.namespaceSelector.matchExpressions"
add-label="Add Namespace Selector"
:mode="mode"
/>
</div>
</div>
</Tab>
<Tab
name="rules"
:label="t('gatekeeperConstraint.tab.rules.title')"
:weight="2"
>
<div class="row">
<div class="col span-12">
<MatchKinds
v-model:value="value.spec.match.kinds"
:mode="mode"
/>
</div>
</div>
<div class="row mt-40">
<div class="col span-12">
<h3>{{ t('gatekeeperConstraint.tab.rules.sub.labelSelector.title') }}</h3>
<RuleSelector
v-model:value="value.spec.match.labelSelector.matchExpressions"
:add-label="t('gatekeeperConstraint.tab.rules.sub.labelSelector.addLabel')"
:mode="mode"
/>
</div>
</div>
</Tab>
<Tab
name="parameters"
:label="t('gatekeeperConstraint.tab.parameters.title')"
:weight="1"
>
<YamlEditor
ref="yamlEditor"
v-model:value="parametersYaml"
class="yaml-editor"
:editor-mode="editorMode"
@newObject="setParameters"
/>
</Tab>
</Tabbed>
</div>
</CruResource>
</template>