Commit baaa2924 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '273787-generalize-policy-editor' into 'master'

Refactor network policy editor to be more generic

See merge request gitlab-org/gitlab!63341
parents 97f986a1 e46499c5
<script>
import { __ } from '~/locale';
import fromYaml, { removeUnnecessaryDashes } from '../policy_editor/lib/from_yaml';
import humanizeNetworkPolicy from '../policy_editor/lib/humanize';
import {
fromYaml,
humanizeNetworkPolicy,
removeUnnecessaryDashes,
} from '../policy_editor/network_policy/lib';
import PolicyPreview from '../policy_editor/policy_preview.vue';
export default {
......
<script>
import { GlButton, GlDrawer } from '@gitlab/ui';
import { getContentWrapperHeight } from '../../utils';
import { CiliumNetworkPolicyKind } from '../policy_editor/constants';
import { CiliumNetworkPolicyKind } from '../policy_editor/network_policy/lib';
import CiliumNetworkPolicy from './cilium_network_policy.vue';
export default {
components: {
GlButton,
GlDrawer,
NetworkPolicyEditor: () =>
import(/* webpackChunkName: 'network_policy_editor' */ '../network_policy_editor.vue'),
PolicyYamlEditor: () =>
import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'),
CiliumNetworkPolicy,
},
props: {
......@@ -70,9 +70,9 @@ export default {
{{ s__("NetworkPolicies|Define this policy's location, conditions and actions.") }}
</p>
<div class="gl-p-3 gl-bg-gray-50">
<network-policy-editor
<policy-yaml-editor
:value="policy.manifest"
data-testid="policyEditor"
data-testid="policy-yaml-editor"
class="network-policy-editor"
/>
</div>
......
......@@ -3,41 +3,14 @@ import { s__ } from '~/locale';
export const EditorModeRule = 'rule';
export const EditorModeYAML = 'yaml';
export const RuleTypeNetwork = 'network';
export const RuleActionTypeAllow = 'allow';
export const RuleDirectionInbound = 'ingress';
export const RuleDirectionOutbound = 'egress';
export const EndpointMatchModeAny = 'any';
export const EndpointMatchModeLabel = 'label';
export const RuleTypeEndpoint = 'NetworkPolicyRuleEndpoint';
export const RuleTypeEntity = 'NetworkPolicyRuleEntity';
export const RuleTypeCIDR = 'NetworkPolicyRuleCIDR';
export const RuleTypeFQDN = 'NetworkPolicyRuleFQDN';
export const EntityTypes = {
ALL: 'all',
HOST: 'host',
REMOTE_NODE: 'remote-node',
CLUSTER: 'cluster',
INIT: 'init',
HEALTH: 'health',
UNMANAGED: 'unmanaged',
WORLD: 'world',
};
export const PortMatchModeAny = 'any';
export const PortMatchModePortProtocol = 'port/protocol';
export const DisabledByLabel = 'network-policy.gitlab.com/disabled_by';
export const CiliumNetworkPolicyKind = 'CiliumNetworkPolicy';
export const ProjectIdLabel = 'app.gitlab.com/proj';
export const PARSING_ERROR_MESSAGE = s__(
'NetworkPolicies|Rule mode is unavailable for this policy. In some cases, we cannot parse the YAML file back into the rules editor.',
);
export const POLICY_TYPES = {
networkPolicy: {
value: 'networkPolicy',
text: s__('NetworkPolicies|Network Policy'),
component: 'network-policy-editor',
},
};
export const RuleTypeNetwork = 'network';
export const RuleActionTypeAllow = 'allow';
export const RuleDirectionInbound = 'ingress';
export const RuleDirectionOutbound = 'egress';
export const EndpointMatchModeAny = 'any';
export const EndpointMatchModeLabel = 'label';
export const RuleTypeEndpoint = 'NetworkPolicyRuleEndpoint';
export const RuleTypeEntity = 'NetworkPolicyRuleEntity';
export const RuleTypeCIDR = 'NetworkPolicyRuleCIDR';
export const RuleTypeFQDN = 'NetworkPolicyRuleFQDN';
export const CiliumNetworkPolicyKind = 'CiliumNetworkPolicy';
export const EntityTypes = {
ALL: 'all',
HOST: 'host',
REMOTE_NODE: 'remote-node',
CLUSTER: 'cluster',
INIT: 'init',
HEALTH: 'health',
UNMANAGED: 'unmanaged',
WORLD: 'world',
};
export const PortMatchModeAny = 'any';
export const PortMatchModePortProtocol = 'port/protocol';
export const DisabledByLabel = 'network-policy.gitlab.com/disabled_by';
export const ProjectIdLabel = 'app.gitlab.com/proj';
......@@ -11,7 +11,7 @@ import {
RuleTypeEntity,
RuleTypeCIDR,
RuleTypeFQDN,
} from '../constants';
} from './constants';
import { buildRule } from './rules';
/*
......
......@@ -6,7 +6,7 @@ import {
RuleTypeEntity,
RuleTypeCIDR,
RuleTypeFQDN,
} from '../constants';
} from './constants';
import { portSelectors, labelSelector, splitItems } from './utils';
const strongArgs = { strongOpen: '<strong>', strongClose: '</strong>' };
......
import { EndpointMatchModeAny } from './constants';
export * from './constants';
export { default as fromYaml, removeUnnecessaryDashes } from './from_yaml';
export { default as humanizeNetworkPolicy } from './humanize';
export { buildRule } from './rules';
export { default as toYaml } from './to_yaml';
export const DEFAULT_NETWORK_POLICY = {
name: '',
description: '',
isEnabled: false,
endpointMatchMode: EndpointMatchModeAny,
endpointLabels: '',
annotations: '',
labels: '',
};
......@@ -5,7 +5,7 @@ import {
RuleTypeFQDN,
RuleDirectionInbound,
PortMatchModeAny,
} from '../constants';
} from './constants';
import { portSelectors, labelSelector, splitItems } from './utils';
/*
......
import { safeDump } from 'js-yaml';
import { EndpointMatchModeAny, DisabledByLabel, CiliumNetworkPolicyKind } from '../constants';
import { EndpointMatchModeAny, DisabledByLabel, CiliumNetworkPolicyKind } from './constants';
import { ruleSpec } from './rules';
import { labelSelector } from './utils';
......
import { PortMatchModeAny } from '../constants';
import { PortMatchModeAny } from './constants';
/*
Convert space separated list of labels into a kubernetes selector.
......
<script>
import {
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlSegmentedControl,
GlButton,
GlAlert,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import { EditorModeRule, EditorModeYAML, PARSING_ERROR_MESSAGE } from '../constants';
import DimDisableContainer from '../dim_disable_container.vue';
import PolicyActionPicker from '../policy_action_picker.vue';
import PolicyAlertPicker from '../policy_alert_picker.vue';
import PolicyPreview from '../policy_preview.vue';
import {
DEFAULT_NETWORK_POLICY,
RuleTypeEndpoint,
ProjectIdLabel,
fromYaml,
removeUnnecessaryDashes,
humanizeNetworkPolicy,
buildRule,
toYaml,
} from './lib';
import PolicyRuleBuilder from './policy_rule_builder.vue';
export default {
i18n: {
toggleLabel: s__('NetworkPolicies|Policy status'),
PARSING_ERROR_MESSAGE,
},
components: {
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlSegmentedControl,
GlButton,
GlAlert,
GlModal,
PolicyYamlEditor: () =>
import(/* webpackChunkName: 'policy_yaml_editor' */ '../../policy_yaml_editor.vue'),
PolicyRuleBuilder,
PolicyPreview,
PolicyActionPicker,
PolicyAlertPicker,
DimDisableContainer,
},
directives: { GlModal: GlModalDirective },
props: {
threatMonitoringPath: {
type: String,
required: true,
},
existingPolicy: {
type: Object,
required: false,
default: null,
},
projectId: {
type: String,
required: true,
},
},
data() {
const policy = this.existingPolicy
? fromYaml(this.existingPolicy.manifest)
: { ...DEFAULT_NETWORK_POLICY, rules: [buildRule()] };
policy.labels = { [ProjectIdLabel]: this.projectId };
const yamlEditorValue = this.existingPolicy
? removeUnnecessaryDashes(this.existingPolicy.manifest)
: '';
return {
editorMode: EditorModeRule,
yamlEditorValue,
yamlEditorError: policy.error ? true : null,
policy,
};
},
computed: {
humanizedPolicy() {
return this.policy.error ? null : humanizeNetworkPolicy(this.policy);
},
policyAlert() {
return Boolean(this.policy.annotations);
},
policyYaml() {
return this.hasParsingError ? '' : toYaml(this.policy);
},
...mapState('threatMonitoring', ['currentEnvironmentId']),
...mapState('networkPolicies', [
'isUpdatingPolicy',
'isRemovingPolicy',
'errorUpdatingPolicy',
'errorRemovingPolicy',
]),
shouldShowRuleEditor() {
return this.editorMode === EditorModeRule;
},
shouldShowYamlEditor() {
return this.editorMode === EditorModeYAML;
},
hasParsingError() {
return Boolean(this.yamlEditorError);
},
isEditing() {
return Boolean(this.existingPolicy);
},
saveButtonText() {
return this.isEditing
? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy');
},
deleteModalTitle() {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policy.name });
},
},
methods: {
...mapActions('networkPolicies', ['createPolicy', 'updatePolicy', 'deletePolicy']),
addRule() {
this.policy.rules.push(buildRule(RuleTypeEndpoint));
},
handleAlertUpdate(includeAlert) {
this.policy.annotations = includeAlert ? { 'app.gitlab.com/alert': 'true' } : '';
},
isNotFirstRule(index) {
return index > 0;
},
updateEndpointMatchMode(mode) {
this.policy.endpointMatchMode = mode;
},
updateEndpointLabels(labels) {
this.policy.endpointLabels = labels;
},
updateRuleType(ruleIndex, ruleType) {
const rule = this.policy.rules[ruleIndex];
this.policy.rules.splice(ruleIndex, 1, buildRule(ruleType, rule));
},
removeRule(ruleIndex) {
this.policy.rules.splice(ruleIndex, 1);
},
loadYaml(manifest) {
this.yamlEditorValue = manifest;
this.yamlEditorError = null;
try {
const newPolicy = fromYaml(manifest);
if (newPolicy.error) {
throw new Error(newPolicy.error);
}
Object.assign(this.policy, newPolicy);
} catch (error) {
this.yamlEditorError = error;
}
},
changeEditorMode(mode) {
if (mode === EditorModeYAML && !this.hasParsingError) {
this.yamlEditorValue = toYaml(this.policy);
}
this.editorMode = mode;
},
savePolicy() {
const saveFn = this.isEditing ? this.updatePolicy : this.createPolicy;
const policy = {
manifest: this.editorMode === EditorModeYAML ? this.yamlEditorValue : toYaml(this.policy),
};
if (this.isEditing) {
policy.name = this.existingPolicy.name;
}
return saveFn({ environmentId: this.currentEnvironmentId, policy }).then(() => {
if (!this.errorUpdatingPolicy) redirectTo(this.threatMonitoringPath);
});
},
removePolicy() {
const policy = { name: this.existingPolicy.name, manifest: toYaml(this.policy) };
return this.deletePolicy({ environmentId: this.currentEnvironmentId, policy }).then(() => {
if (!this.errorRemovingPolicy) redirectTo(this.threatMonitoringPath);
});
},
},
policyTypes: [{ value: 'networkPolicy', text: s__('NetworkPolicies|Network Policy') }],
editorModes: [
{ value: EditorModeRule, text: s__('NetworkPolicies|Rule mode') },
{ value: EditorModeYAML, text: s__('NetworkPolicies|.yaml mode') },
],
deleteModal: {
id: 'delete-modal',
secondary: {
text: s__('NetworkPolicies|Delete policy'),
attributes: { variant: 'danger' },
},
cancel: {
text: __('Cancel'),
},
},
};
</script>
<template>
<section>
<div class="gl-mb-5 gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base">
<gl-form-group
class="gl-px-5 gl-py-3 gl-mb-0 gl-bg-gray-10 gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
>
<gl-segmented-control
data-testid="editor-mode"
:options="$options.editorModes"
:checked="editorMode"
@input="changeEditorMode"
/>
</gl-form-group>
<div class="gl-display-flex gl-sm-flex-direction-column">
<section class="gl-w-full gl-p-5 gl-flex-fill-4 policy-table-left">
<div v-if="shouldShowRuleEditor" data-testid="rule-editor">
<gl-alert v-if="hasParsingError" data-testid="parsing-alert" :dismissible="false">
{{ $options.i18n.PARSING_ERROR_MESSAGE }}
</gl-alert>
<gl-form-group :label="s__('NetworkPolicies|Name')" label-for="policyName">
<gl-form-input id="policyName" v-model="policy.name" :disabled="hasParsingError" />
</gl-form-group>
<gl-form-group
:label="s__('NetworkPolicies|Description')"
label-for="policyDescription"
>
<gl-form-textarea
id="policyDescription"
v-model="policy.description"
:disabled="hasParsingError"
/>
</gl-form-group>
<gl-form-group :disabled="hasParsingError" data-testid="policy-enable">
<gl-toggle v-model="policy.isEnabled" :label="$options.i18n.toggleLabel" />
</gl-form-group>
<dim-disable-container data-testid="rule-builder-container" :disabled="hasParsingError">
<template #title>
<h4>{{ s__('NetworkPolicies|Rules') }}</h4>
</template>
<template #disabled>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-6"
></div>
</template>
<policy-rule-builder
v-for="(rule, index) in policy.rules"
:key="index"
class="gl-mb-4"
:rule="rule"
:endpoint-match-mode="policy.endpointMatchMode"
:endpoint-labels="policy.endpointLabels"
:endpoint-selector-disabled="isNotFirstRule(index)"
@rule-type-change="updateRuleType(index, $event)"
@endpoint-match-mode-change="updateEndpointMatchMode"
@endpoint-labels-change="updateEndpointLabels"
@remove="removeRule(index)"
/>
<div
class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5"
>
<gl-button variant="link" data-testid="add-rule" @click="addRule">{{
s__('Network Policy|New rule')
}}</gl-button>
</div>
</dim-disable-container>
<dim-disable-container
data-testid="policy-action-container"
:disabled="hasParsingError"
>
<template #title>
<h4>{{ s__('NetworkPolicies|Actions') }}</h4>
<p>
{{ s__('NetworkPolicies|Traffic that does not match any rule will be blocked.') }}
</p>
</template>
<template #disabled>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-6"
></div>
</template>
<policy-action-picker />
<policy-alert-picker :policy-alert="policyAlert" @update-alert="handleAlertUpdate" />
</dim-disable-container>
</div>
<policy-yaml-editor
v-if="shouldShowYamlEditor"
data-testid="policy-yaml-editor"
:value="yamlEditorValue"
:read-only="false"
@input="loadYaml"
/>
</section>
<section
v-if="shouldShowRuleEditor"
class="gl-w-30p gl-p-5 gl-border-l-gray-100 gl-border-l-1 gl-border-l-solid gl-flex-fill-2"
>
<dim-disable-container data-testid="policy-preview-container" :disabled="hasParsingError">
<template #title>
<h5>{{ s__('NetworkPolicies|Policy preview') }}</h5>
</template>
<template #disabled>
<policy-preview :policy-yaml="s__('NetworkPolicies|Unable to parse policy')" />
</template>
<policy-preview :policy-yaml="policyYaml" :policy-description="humanizedPolicy" />
</dim-disable-container>
</section>
</div>
</div>
<div>
<gl-button
type="submit"
variant="success"
data-testid="save-policy"
:loading="isUpdatingPolicy"
@click="savePolicy"
>{{ saveButtonText }}</gl-button
>
<gl-button
v-if="isEditing"
v-gl-modal="'delete-modal'"
category="secondary"
variant="danger"
data-testid="delete-policy"
:loading="isRemovingPolicy"
>{{ s__('NetworkPolicies|Delete policy') }}</gl-button
>
<gl-button category="secondary" :href="threatMonitoringPath">{{ __('Cancel') }}</gl-button>
</div>
<gl-modal
modal-id="delete-modal"
:title="deleteModalTitle"
:action-secondary="$options.deleteModal.secondary"
:action-cancel="$options.deleteModal.cancel"
@secondary="removePolicy"
>
{{
s__(
'NetworkPolicies|Are you sure you want to delete this policy? This action cannot be undone.',
)
}}
</gl-modal>
</section>
</template>
......@@ -13,7 +13,7 @@ import {
RuleTypeFQDN,
PortMatchModeAny,
PortMatchModePortProtocol,
} from './constants';
} from './lib';
import PolicyRuleCIDR from './policy_rule_cidr.vue';
import PolicyRuleEndpoint from './policy_rule_endpoint.vue';
import PolicyRuleEntity from './policy_rule_entity.vue';
......
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { EntityTypes } from './constants';
import { EntityTypes } from './lib';
export default {
components: {
......
<script>
import { GlForm, GlFormSelect, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import { RuleActionTypeAllow } from './constants';
import { RuleActionTypeAllow } from './network_policy/lib';
export default {
components: {
......
<script>
import {
GlFormGroup,
GlFormSelect,
GlFormInput,
GlFormTextarea,
GlToggle,
GlSegmentedControl,
GlButton,
GlAlert,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
import { mapActions } from 'vuex';
import EnvironmentPicker from '../environment_picker.vue';
import {
EditorModeRule,
EditorModeYAML,
EndpointMatchModeAny,
RuleTypeEndpoint,
ProjectIdLabel,
PARSING_ERROR_MESSAGE,
} from './constants';
import DimDisableContainer from './dim_disable_container.vue';
import fromYaml, { removeUnnecessaryDashes } from './lib/from_yaml';
import humanizeNetworkPolicy from './lib/humanize';
import { buildRule } from './lib/rules';
import toYaml from './lib/to_yaml';
import PolicyActionPicker from './policy_action_picker.vue';
import PolicyAlertPicker from './policy_alert_picker.vue';
import PolicyPreview from './policy_preview.vue';
import PolicyRuleBuilder from './policy_rule_builder.vue';
import { POLICY_TYPES } from './constants';
import NetworkPolicyEditor from './network_policy/network_policy_editor.vue';
export default {
i18n: {
toggleLabel: s__('NetworkPolicies|Policy status'),
PARSING_ERROR_MESSAGE,
},
components: {
GlFormGroup,
GlFormSelect,
GlFormInput,
GlFormTextarea,
GlToggle,
GlSegmentedControl,
GlButton,
GlAlert,
GlModal,
EnvironmentPicker,
NetworkPolicyEditor: () =>
import(/* webpackChunkName: 'network_policy_editor' */ '../network_policy_editor.vue'),
PolicyRuleBuilder,
PolicyPreview,
PolicyActionPicker,
PolicyAlertPicker,
DimDisableContainer,
NetworkPolicyEditor,
},
directives: { GlModal: GlModalDirective },
props: {
threatMonitoringPath: {
type: String,
......@@ -74,67 +28,13 @@ export default {
},
},
data() {
const policy = this.existingPolicy
? fromYaml(this.existingPolicy.manifest)
: {
name: '',
description: '',
isEnabled: false,
endpointMatchMode: EndpointMatchModeAny,
endpointLabels: '',
rules: [buildRule(RuleTypeEndpoint)],
annotations: '',
labels: '',
};
policy.labels = { [ProjectIdLabel]: this.projectId };
const yamlEditorValue = this.existingPolicy
? removeUnnecessaryDashes(this.existingPolicy.manifest)
: '';
return {
editorMode: EditorModeRule,
yamlEditorValue,
yamlEditorError: policy.error ? true : null,
policy,
policyType: POLICY_TYPES.networkPolicy.value,
};
},
computed: {
humanizedPolicy() {
return this.policy.error ? null : humanizeNetworkPolicy(this.policy);
},
policyAlert() {
return Boolean(this.policy.annotations);
},
policyYaml() {
return this.hasParsingError ? '' : toYaml(this.policy);
},
...mapState('threatMonitoring', ['currentEnvironmentId']),
...mapState('networkPolicies', [
'isUpdatingPolicy',
'isRemovingPolicy',
'errorUpdatingPolicy',
'errorRemovingPolicy',
]),
shouldShowRuleEditor() {
return this.editorMode === EditorModeRule;
},
shouldShowYamlEditor() {
return this.editorMode === EditorModeYAML;
},
hasParsingError() {
return Boolean(this.yamlEditorError);
},
isEditing() {
return Boolean(this.existingPolicy);
},
saveButtonText() {
return this.isEditing
? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy');
},
deleteModalTitle() {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policy.name });
policyComponent() {
return POLICY_TYPES[this.policyType].component;
},
},
created() {
......@@ -142,86 +42,11 @@ export default {
},
methods: {
...mapActions('threatMonitoring', ['fetchEnvironments']),
...mapActions('networkPolicies', ['createPolicy', 'updatePolicy', 'deletePolicy']),
addRule() {
this.policy.rules.push(buildRule(RuleTypeEndpoint));
},
handleAlertUpdate(includeAlert) {
this.policy.annotations = includeAlert ? { 'app.gitlab.com/alert': 'true' } : '';
},
isNotFirstRule(index) {
return index > 0;
},
updateEndpointMatchMode(mode) {
this.policy.endpointMatchMode = mode;
},
updateEndpointLabels(labels) {
this.policy.endpointLabels = labels;
},
updateRuleType(ruleIndex, ruleType) {
const rule = this.policy.rules[ruleIndex];
this.policy.rules.splice(ruleIndex, 1, buildRule(ruleType, rule));
},
removeRule(ruleIndex) {
this.policy.rules.splice(ruleIndex, 1);
},
loadYaml(manifest) {
this.yamlEditorValue = manifest;
this.yamlEditorError = null;
try {
const newPolicy = fromYaml(manifest);
if (newPolicy.error) {
throw new Error(newPolicy.error);
}
Object.assign(this.policy, newPolicy);
} catch (error) {
this.yamlEditorError = error;
}
},
changeEditorMode(mode) {
if (mode === EditorModeYAML && !this.hasParsingError) {
this.yamlEditorValue = toYaml(this.policy);
}
this.editorMode = mode;
},
savePolicy() {
const saveFn = this.isEditing ? this.updatePolicy : this.createPolicy;
const policy = {
manifest: this.editorMode === EditorModeYAML ? this.yamlEditorValue : toYaml(this.policy),
};
if (this.isEditing) {
policy.name = this.existingPolicy.name;
}
return saveFn({ environmentId: this.currentEnvironmentId, policy }).then(() => {
if (!this.errorUpdatingPolicy) redirectTo(this.threatMonitoringPath);
});
},
removePolicy() {
const policy = { name: this.existingPolicy.name, manifest: toYaml(this.policy) };
return this.deletePolicy({ environmentId: this.currentEnvironmentId, policy }).then(() => {
if (!this.errorRemovingPolicy) redirectTo(this.threatMonitoringPath);
});
},
},
policyTypes: [{ value: 'networkPolicy', text: s__('NetworkPolicies|Network Policy') }],
editorModes: [
{ value: EditorModeRule, text: s__('NetworkPolicies|Rule mode') },
{ value: EditorModeYAML, text: s__('NetworkPolicies|.yaml mode') },
],
deleteModal: {
id: 'delete-modal',
secondary: {
text: s__('NetworkPolicies|Delete policy'),
attributes: { variant: 'danger' },
},
cancel: {
text: __('Cancel'),
updatePolicyType(type) {
this.policyType = type;
},
},
policyTypes: Object.values(POLICY_TYPES),
};
</script>
......@@ -234,166 +59,19 @@ export default {
<gl-form-group :label="s__('NetworkPolicies|Policy type')" label-for="policyType">
<gl-form-select
id="policyType"
value="networkPolicy"
:value="policyType"
:options="$options.policyTypes"
disabled
@change="updatePolicyType"
/>
</gl-form-group>
<environment-picker />
</div>
<div class="gl-mb-5 gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base">
<gl-form-group
class="gl-px-5 gl-py-3 gl-mb-0 gl-bg-gray-10 gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
>
<gl-segmented-control
data-testid="editor-mode"
:options="$options.editorModes"
:checked="editorMode"
@input="changeEditorMode"
/>
</gl-form-group>
<div class="gl-display-flex gl-sm-flex-direction-column">
<section class="gl-w-full gl-p-5 gl-flex-fill-4 policy-table-left">
<div v-if="shouldShowRuleEditor" data-testid="rule-editor">
<gl-alert v-if="hasParsingError" data-testid="parsing-alert" :dismissible="false">
{{ $options.i18n.PARSING_ERROR_MESSAGE }}
</gl-alert>
<gl-form-group :label="s__('NetworkPolicies|Name')" label-for="policyName">
<gl-form-input id="policyName" v-model="policy.name" :disabled="hasParsingError" />
</gl-form-group>
<gl-form-group
:label="s__('NetworkPolicies|Description')"
label-for="policyDescription"
>
<gl-form-textarea
id="policyDescription"
v-model="policy.description"
:disabled="hasParsingError"
/>
</gl-form-group>
<gl-form-group :disabled="hasParsingError" data-testid="policy-enable">
<gl-toggle v-model="policy.isEnabled" :label="$options.i18n.toggleLabel" />
</gl-form-group>
<dim-disable-container data-testid="rule-builder-container" :disabled="hasParsingError">
<template #title>
<h4>{{ s__('NetworkPolicies|Rules') }}</h4>
</template>
<template #disabled>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-6"
></div>
</template>
<policy-rule-builder
v-for="(rule, index) in policy.rules"
:key="index"
class="gl-mb-4"
:rule="rule"
:endpoint-match-mode="policy.endpointMatchMode"
:endpoint-labels="policy.endpointLabels"
:endpoint-selector-disabled="isNotFirstRule(index)"
@rule-type-change="updateRuleType(index, $event)"
@endpoint-match-mode-change="updateEndpointMatchMode"
@endpoint-labels-change="updateEndpointLabels"
@remove="removeRule(index)"
/>
<div
class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5"
>
<gl-button variant="link" data-testid="add-rule" @click="addRule">{{
s__('Network Policy|New rule')
}}</gl-button>
</div>
</dim-disable-container>
<dim-disable-container
data-testid="policy-action-container"
:disabled="hasParsingError"
>
<template #title>
<h4>{{ s__('NetworkPolicies|Actions') }}</h4>
<p>
{{ s__('NetworkPolicies|Traffic that does not match any rule will be blocked.') }}
</p>
</template>
<template #disabled>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-6"
></div>
</template>
<policy-action-picker />
<policy-alert-picker :policy-alert="policyAlert" @update-alert="handleAlertUpdate" />
</dim-disable-container>
</div>
<network-policy-editor
v-if="shouldShowYamlEditor"
data-testid="network-policy-editor"
:value="yamlEditorValue"
:read-only="false"
@input="loadYaml"
/>
</section>
<section
v-if="shouldShowRuleEditor"
class="gl-w-30p gl-p-5 gl-border-l-gray-100 gl-border-l-1 gl-border-l-solid gl-flex-fill-2"
>
<dim-disable-container data-testid="policy-preview-container" :disabled="hasParsingError">
<template #title>
<h5>{{ s__('NetworkPolicies|Policy preview') }}</h5>
</template>
<template #disabled>
<policy-preview :policy-yaml="s__('NetworkPolicies|Unable to parse policy')" />
</template>
<policy-preview :policy-yaml="policyYaml" :policy-description="humanizedPolicy" />
</dim-disable-container>
</section>
</div>
</div>
<div>
<gl-button
type="submit"
variant="success"
data-testid="save-policy"
:loading="isUpdatingPolicy"
@click="savePolicy"
>{{ saveButtonText }}</gl-button
>
<gl-button
v-if="isEditing"
v-gl-modal="'delete-modal'"
category="secondary"
variant="danger"
data-testid="delete-policy"
:loading="isRemovingPolicy"
>{{ s__('NetworkPolicies|Delete policy') }}</gl-button
>
<gl-button category="secondary" :href="threatMonitoringPath">{{ __('Cancel') }}</gl-button>
</div>
<gl-modal
modal-id="delete-modal"
:title="deleteModalTitle"
:action-secondary="$options.deleteModal.secondary"
:action-cancel="$options.deleteModal.cancel"
@secondary="removePolicy"
>
{{
s__(
'NetworkPolicies|Are you sure you want to delete this policy? This action cannot be undone.',
)
}}
</gl-modal>
<component
:is="policyComponent"
:threat-monitoring-path="threatMonitoringPath"
:existing-policy="existingPolicy"
:project-id="projectId"
/>
</section>
</template>
import CiliumNetworkPolicy from 'ee/threat_monitoring/components/policy_drawer/cilium_network_policy.vue';
import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml';
import { toYaml } from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
import PolicyPreview from 'ee/threat_monitoring/components/policy_editor/policy_preview.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
......
......@@ -15,13 +15,13 @@ describe('NetworkPolicyDrawer component', () => {
open: true,
...propsData,
},
stubs: { NetworkPolicyEditor: true },
stubs: { PolicyYamlEditor: true },
});
};
// Finders
const findEditButton = () => wrapper.findByTestId('edit-button');
const findPolicyEditor = () => wrapper.findByTestId('policyEditor');
const findPolicyEditor = () => wrapper.findByTestId('policy-yaml-editor');
const findCiliumNetworkPolicy = () => wrapper.findComponent(CiliumNetworkPolicy);
// Shared assertions
......
......@@ -10,12 +10,11 @@ import {
RuleTypeCIDR,
RuleTypeFQDN,
EntityTypes,
} from 'ee/threat_monitoring/components/policy_editor/constants';
import fromYaml, {
fromYaml,
removeUnnecessaryDashes,
} from 'ee/threat_monitoring/components/policy_editor/lib/from_yaml';
import { buildRule } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml';
buildRule,
toYaml,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
describe('fromYaml', () => {
let policy;
......
......@@ -6,9 +6,9 @@ import {
RuleTypeEntity,
RuleTypeCIDR,
RuleTypeFQDN,
} from 'ee/threat_monitoring/components/policy_editor/constants';
import humanizeNetworkPolicy from 'ee/threat_monitoring/components/policy_editor/lib/humanize';
import { buildRule } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
humanizeNetworkPolicy,
buildRule,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
describe('humanizeNetworkPolicy', () => {
let policy;
......
......@@ -8,8 +8,9 @@ import {
PortMatchModeAny,
PortMatchModePortProtocol,
EntityTypes,
} from 'ee/threat_monitoring/components/policy_editor/constants';
import { buildRule, ruleSpec } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
buildRule,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
import { ruleSpec } from 'ee/threat_monitoring/components/policy_editor/network_policy/lib/rules';
describe('buildRule', () => {
const oldRule = {
......
import { EndpointMatchModeLabel } from 'ee/threat_monitoring/components/policy_editor/constants';
import { buildRule } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml';
import {
EndpointMatchModeLabel,
buildRule,
toYaml,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
describe('toYaml', () => {
let policy;
......
import {
PortMatchModeAny,
PortMatchModePortProtocol,
} from 'ee/threat_monitoring/components/policy_editor/constants';
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
import {
labelSelector,
portSelectors,
splitItems,
} from 'ee/threat_monitoring/components/policy_editor/lib/utils';
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib/utils';
describe('labelSelector', () => {
it('returns selector map', () => {
......
import { GlModal, GlToggle } from '@gitlab/ui';
import { EditorModeYAML } from 'ee/threat_monitoring/components/policy_editor/constants';
import {
RuleDirectionInbound,
PortMatchModeAny,
RuleTypeEndpoint,
EndpointMatchModeLabel,
fromYaml,
buildRule,
toYaml,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
import NetworkPolicyEditor from 'ee/threat_monitoring/components/policy_editor/network_policy/network_policy_editor.vue';
import PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/network_policy/policy_rule_builder.vue';
import PolicyAlertPicker from 'ee/threat_monitoring/components/policy_editor/policy_alert_picker.vue';
import PolicyPreview from 'ee/threat_monitoring/components/policy_editor/policy_preview.vue';
import createStore from 'ee/threat_monitoring/store';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
describe('NetworkPolicyEditor component', () => {
let store;
let wrapper;
const l7manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: limit-inbound-ip
spec:
endpointSelector: {}
ingress:
- toPorts:
- ports:
- port: '80'
protocol: TCP
- port: '443'
protocol: TCP
rules:
http:
- headers:
- 'X-Forwarded-For: 192.168.1.1'
fromEntities:
- cluster`;
const factory = ({ propsData, provide = {}, state, data } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
...state,
});
Object.assign(store.state.networkPolicies, {
...state,
});
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = shallowMountExtended(NetworkPolicyEditor, {
propsData: {
threatMonitoringPath: '/threat-monitoring',
projectId: '21',
...propsData,
},
provide: {
...provide,
},
store,
data,
stubs: { PolicyYamlEditor: true },
});
};
const findRuleEditor = () => wrapper.findByTestId('rule-editor');
const findPreview = () => wrapper.findComponent(PolicyPreview);
const findAddRuleButton = () => wrapper.findByTestId('add-rule');
const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert');
const findPolicyYamlEditor = () => wrapper.findByTestId('policy-yaml-editor');
const findPolicyAlertPicker = () => wrapper.findComponent(PolicyAlertPicker);
const findPolicyDescription = () => wrapper.find("[id='policyDescription']");
const findPolicyEnableContainer = () => wrapper.findByTestId('policy-enable');
const findPolicyName = () => wrapper.find("[id='policyName']");
const findPolicyRuleBuilder = () => wrapper.findComponent(PolicyRuleBuilder);
const findSavePolicy = () => wrapper.findByTestId('save-policy');
const findDeletePolicy = () => wrapper.findByTestId('delete-policy');
const findEditorModeToggle = () => wrapper.findByTestId('editor-mode');
const modifyPolicyAlert = async ({ isAlertEnabled }) => {
const policyAlertPicker = findPolicyAlertPicker();
policyAlertPicker.vm.$emit('update-alert', isAlertEnabled);
await wrapper.vm.$nextTick();
expect(policyAlertPicker.props('policyAlert')).toBe(isAlertEnabled);
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
};
beforeEach(() => {
factory();
});
afterEach(() => {
wrapper.destroy();
});
it('renders toggle with label', () => {
const policyEnableToggle = findPolicyEnableContainer().findComponent(GlToggle);
expect(policyEnableToggle.exists()).toBe(true);
expect(policyEnableToggle.props('label')).toBe(NetworkPolicyEditor.i18n.toggleLabel);
});
it('renders a default rule with label', () => {
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
expect(findPolicyRuleBuilder().exists()).toBe(true);
expect(findPolicyRuleBuilder().attributes()).toMatchObject({
endpointlabels: '',
endpointmatchmode: 'any',
});
});
it.each`
component | status | findComponent | state
${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true}
${'editor mode toggle'} | ${'does display'} | ${findEditorModeToggle} | ${true}
${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true}
${'rule editor'} | ${'does display'} | ${findRuleEditor} | ${true}
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'yaml editor'} | ${'does not display'} | ${findPolicyYamlEditor} | ${false}
${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false}
${'delete button'} | ${'does not display'} | ${findDeletePolicy} | ${false}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
describe('given .yaml editor mode is enabled', () => {
beforeEach(() => {
factory({
data: () => ({
editorMode: EditorModeYAML,
}),
});
});
it.each`
component | status | findComponent | state
${'editor mode toggle'} | ${'does display'} | ${findEditorModeToggle} | ${true}
${'rule editor'} | ${'does not display'} | ${findRuleEditor} | ${false}
${'yaml editor'} | ${'does display'} | ${findPolicyYamlEditor} | ${true}
`('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('updates policy on yaml editor value change', async () => {
const manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
description: test description
metadata:
name: test-policy
labels:
app.gitlab.com/proj: '21'
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
foo: bar
ingress:
- fromEndpoints:
- matchLabels:
foo: bar`;
findPolicyYamlEditor().vm.$emit('input', manifest);
expect(wrapper.vm.policy).toMatchObject({
name: 'test-policy',
description: 'test description',
isEnabled: false,
endpointMatchMode: EndpointMatchModeLabel,
endpointLabels: 'foo:bar',
rules: [
{
ruleType: RuleTypeEndpoint,
matchLabels: 'foo:bar',
},
],
labels: { 'app.gitlab.com/proj': '21' },
});
});
it('saves L7 policies', async () => {
factory({
data: () => ({
editorMode: EditorModeYAML,
yamlEditorValue: l7manifest,
}),
});
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: { manifest: l7manifest },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
});
it('given there is a name change, updates policy yaml preview', async () => {
const initialValue = findPreview().props('policyYaml');
findPolicyName().vm.$emit('input', 'new');
await wrapper.vm.$nextTick();
expect(findPreview().props('policyYaml')).not.toEqual(initialValue);
});
it('given there is a rule change, updates policy description preview', async () => {
const initialValue = findPreview().props('policyDescription');
findAddRuleButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findPreview().props('policyDescription')).not.toEqual(initialValue);
});
it('adds a new rule', async () => {
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
const button = findAddRuleButton();
button.vm.$emit('click');
button.vm.$emit('click');
await wrapper.vm.$nextTick();
const elements = wrapper.findAllComponents(PolicyRuleBuilder);
expect(elements).toHaveLength(3);
elements.wrappers.forEach((builder, idx) => {
expect(builder.props().rule).toMatchObject({
ruleType: RuleTypeEndpoint,
direction: RuleDirectionInbound,
matchLabels: '',
portMatchMode: PortMatchModeAny,
ports: '',
});
expect(builder.props().endpointSelectorDisabled).toEqual(idx !== 0);
});
});
it('removes a new rule', async () => {
findAddRuleButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(2);
findPolicyRuleBuilder().vm.$emit('remove');
await wrapper.vm.$nextTick();
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
});
it('updates yaml editor value on switch to yaml editor', async () => {
findPolicyName().vm.$emit('input', 'test-policy');
findEditorModeToggle().vm.$emit('input', EditorModeYAML);
await wrapper.vm.$nextTick();
const editor = findPolicyYamlEditor();
expect(editor.exists()).toBe(true);
expect(fromYaml(editor.attributes('value'))).toMatchObject({
name: 'test-policy',
});
});
describe('given there is a yaml parsing error', () => {
beforeEach(() => {
factory({
data: () => ({
yamlEditorError: {},
}),
});
});
it('disables policy name field', () => {
expect(findPolicyName().attributes().disabled).toBe('true');
});
it('disables policy description field', () => {
expect(findPolicyDescription().attributes().disabled).toBe('true');
});
it('disables policy enable/disable toggle', () => {
expect(findPolicyEnableContainer().attributes().disabled).toBe('true');
});
it('renders parsing error alert', () => {
expect(findYAMLParsingAlert().exists()).toBe(true);
});
it('disables rule builder', () => {
expect(wrapper.findByTestId('rule-builder-container').props().disabled).toBe(true);
});
it('disables action picker', () => {
expect(wrapper.findByTestId('policy-action-container').props().disabled).toBe(true);
});
it('disables policy preview', () => {
expect(wrapper.findByTestId('policy-preview-container').props().disabled).toBe(true);
});
it('does not update yaml editor value on switch to yaml editor', async () => {
findPolicyName().vm.$emit('input', 'test-policy');
findEditorModeToggle().vm.$emit('input', EditorModeYAML);
await wrapper.vm.$nextTick();
const editor = findPolicyYamlEditor();
expect(editor.exists()).toBe(true);
expect(editor.attributes('value')).toEqual('');
});
});
it('creates policy and redirects to a threat monitoring path', async () => {
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: { manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
describe('given there is a createPolicy error', () => {
beforeEach(() => {
factory({
state: {
errorUpdatingPolicy: true,
},
});
});
it('it does not redirect', async () => {
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(redirectTo).not.toHaveBeenCalledWith('/threat-monitoring');
});
});
describe('given existingPolicy property was provided', () => {
const manifest = toYaml({
name: 'policy',
endpointLabels: '',
rules: [buildRule()],
});
beforeEach(() => {
factory({
propsData: {
existingPolicy: { name: 'policy', manifest },
},
});
});
it('presents existing policy', () => {
expect(findPolicyName().attributes().value).toEqual('policy');
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
});
it('updates existing policy and redirects to a threat monitoring path', async () => {
const saveButton = findSavePolicy();
expect(saveButton.text()).toEqual('Save changes');
saveButton.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/updatePolicy', {
environmentId: -1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
describe('given there is a updatePolicy error', () => {
beforeEach(() => {
factory({
propsData: {
existingPolicy: { name: 'policy', manifest },
},
state: {
errorUpdatingPolicy: true,
},
});
});
it('it does not redirect', async () => {
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(redirectTo).not.toHaveBeenCalledWith('/threat-monitoring');
});
});
it('renders delete button', () => {
expect(findDeletePolicy().exists()).toBe(true);
});
it('it does not trigger deletePolicy on delete button click', async () => {
findDeletePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).not.toHaveBeenCalledWith('networkPolicies/deletePolicy');
});
it('removes policy and redirects to a threat monitoring path on secondary modal button click', async () => {
wrapper.findComponent(GlModal).vm.$emit('secondary');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/deletePolicy', {
environmentId: -1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
});
describe('add alert picker', () => {
it('adds a policy annotation on alert addition', async () => {
await modifyPolicyAlert({ isAlertEnabled: true });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: {
manifest: expect.stringContaining("app.gitlab.com/alert: 'true'"),
},
});
});
it('removes a policy annotation on alert removal', async () => {
await modifyPolicyAlert({ isAlertEnabled: false });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: {
manifest: expect.not.stringContaining("app.gitlab.com/alert: 'true'"),
},
});
});
});
});
......@@ -7,13 +7,13 @@ import {
RuleTypeCIDR,
RuleTypeFQDN,
PortMatchModePortProtocol,
} from 'ee/threat_monitoring/components/policy_editor/constants';
import { buildRule } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
import PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/policy_rule_builder.vue';
import PolicyRuleCIDR from 'ee/threat_monitoring/components/policy_editor/policy_rule_cidr.vue';
import PolicyRuleEndpoint from 'ee/threat_monitoring/components/policy_editor/policy_rule_endpoint.vue';
import PolicyRuleEntity from 'ee/threat_monitoring/components/policy_editor/policy_rule_entity.vue';
import PolicyRuleFQDN from 'ee/threat_monitoring/components/policy_editor/policy_rule_fqdn.vue';
buildRule,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
import PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/network_policy/policy_rule_builder.vue';
import PolicyRuleCIDR from 'ee/threat_monitoring/components/policy_editor/network_policy/policy_rule_cidr.vue';
import PolicyRuleEndpoint from 'ee/threat_monitoring/components/policy_editor/network_policy/policy_rule_endpoint.vue';
import PolicyRuleEntity from 'ee/threat_monitoring/components/policy_editor/network_policy/policy_rule_entity.vue';
import PolicyRuleFQDN from 'ee/threat_monitoring/components/policy_editor/network_policy/policy_rule_fqdn.vue';
describe('PolicyRuleBuilder component', () => {
let wrapper;
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { EntityTypes } from 'ee/threat_monitoring/components/policy_editor/constants';
import PolicyRuleEntity from 'ee/threat_monitoring/components/policy_editor/policy_rule_entity.vue';
import { EntityTypes } from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
import PolicyRuleEntity from 'ee/threat_monitoring/components/policy_editor/network_policy/policy_rule_entity.vue';
describe('PolicyRuleEntity component', () => {
let wrapper;
......
import { GlModal, GlToggle } from '@gitlab/ui';
import { GlFormSelect } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import {
RuleDirectionInbound,
PortMatchModeAny,
RuleTypeEndpoint,
EditorModeYAML,
EndpointMatchModeLabel,
} from 'ee/threat_monitoring/components/policy_editor/constants';
import fromYaml from 'ee/threat_monitoring/components/policy_editor/lib/from_yaml';
import { buildRule } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml';
import PolicyAlertPicker from 'ee/threat_monitoring/components/policy_editor/policy_alert_picker.vue';
import PolicyEditorApp from 'ee/threat_monitoring/components/policy_editor/policy_editor.vue';
import PolicyPreview from 'ee/threat_monitoring/components/policy_editor/policy_preview.vue';
import PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/policy_rule_builder.vue';
import EnvironmentPicker from 'ee/threat_monitoring/components/environment_picker.vue';
import { POLICY_TYPES } from 'ee/threat_monitoring/components/policy_editor/constants';
import NetworkPolicyEditor from 'ee/threat_monitoring/components/policy_editor/network_policy/network_policy_editor.vue';
import PolicyEditor from 'ee/threat_monitoring/components/policy_editor/policy_editor.vue';
import createStore from 'ee/threat_monitoring/store';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
describe('PolicyEditorApp component', () => {
describe('PolicyEditor component', () => {
let store;
let wrapper;
const l7manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: limit-inbound-ip
spec:
endpointSelector: {}
ingress:
- toPorts:
- ports:
- port: '80'
protocol: TCP
- port: '443'
protocol: TCP
rules:
http:
- headers:
- 'X-Forwarded-For: 192.168.1.1'
fromEntities:
- cluster`;
const factory = ({ propsData, provide = {}, state, data } = {}) => {
const findEnvironmentPicker = () => wrapper.findComponent(EnvironmentPicker);
const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const findNeworkPolicyEditor = () => wrapper.findComponent(NetworkPolicyEditor);
const factory = ({ propsData } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
...state,
});
Object.assign(store.state.networkPolicies, {
...state,
});
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = extendedWrapper(
shallowMount(PolicyEditorApp, {
propsData: {
threatMonitoringPath: '/threat-monitoring',
projectId: '21',
...propsData,
},
provide: {
...provide,
},
store,
data,
stubs: { NetworkPolicyEditor: true },
}),
);
};
const findRuleEditor = () => wrapper.findByTestId('rule-editor');
const findPreview = () => wrapper.findComponent(PolicyPreview);
const findAddRuleButton = () => wrapper.findByTestId('add-rule');
const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert');
const findNetworkPolicyEditor = () => wrapper.findByTestId('network-policy-editor');
const findPolicyAlertPicker = () => wrapper.findComponent(PolicyAlertPicker);
const findPolicyDescription = () => wrapper.find("[id='policyDescription']");
const findPolicyEnableContainer = () => wrapper.findByTestId('policy-enable');
const findPolicyName = () => wrapper.find("[id='policyName']");
const findPolicyRuleBuilder = () => wrapper.findComponent(PolicyRuleBuilder);
const findSavePolicy = () => wrapper.findByTestId('save-policy');
const findDeletePolicy = () => wrapper.findByTestId('delete-policy');
const findEditorModeToggle = () => wrapper.findByTestId('editor-mode');
const modifyPolicyAlert = async ({ isAlertEnabled }) => {
const policyAlertPicker = findPolicyAlertPicker();
policyAlertPicker.vm.$emit('update-alert', isAlertEnabled);
await wrapper.vm.$nextTick();
expect(policyAlertPicker.props('policyAlert')).toBe(isAlertEnabled);
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
wrapper = shallowMount(PolicyEditor, {
propsData: {
threatMonitoringPath: '/threat-monitoring',
projectId: '21',
...propsData,
},
store,
stubs: { GlFormSelect },
});
};
beforeEach(() => {
factory();
});
beforeEach(factory);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders toggle with label', () => {
const policyEnableToggle = findPolicyEnableContainer().findComponent(GlToggle);
expect(policyEnableToggle.exists()).toBe(true);
expect(policyEnableToggle.props('label')).toBe(PolicyEditorApp.i18n.toggleLabel);
});
it('renders a default rule with label', () => {
expect(findPolicyRuleBuilder().exists()).toBe(true);
expect(findPolicyRuleBuilder().attributes()).toMatchObject({
endpointlabels: '',
endpointmatchmode: 'any',
});
});
it.each`
component | status | findComponent | state
${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true}
${'editor mode toggle'} | ${'does display'} | ${findEditorModeToggle} | ${true}
${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true}
${'rule editor'} | ${'does display'} | ${findRuleEditor} | ${true}
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'yaml editor'} | ${'does not display'} | ${findNetworkPolicyEditor} | ${false}
${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false}
${'delete button'} | ${'does not display'} | ${findDeletePolicy} | ${false}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
describe('given .yaml editor mode is enabled', () => {
beforeEach(() => {
factory({
data: () => ({
editorMode: EditorModeYAML,
}),
});
});
it.each`
component | status | findComponent | state
${'editor mode toggle'} | ${'does display'} | ${findEditorModeToggle} | ${true}
${'rule editor'} | ${'does not display'} | ${findRuleEditor} | ${false}
${'yaml editor'} | ${'does display'} | ${findNetworkPolicyEditor} | ${true}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('updates policy on yaml editor value change', async () => {
const manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
description: test description
metadata:
name: test-policy
labels:
app.gitlab.com/proj: '21'
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
foo: bar
ingress:
- fromEndpoints:
- matchLabels:
foo: bar`;
findNetworkPolicyEditor().vm.$emit('input', manifest);
expect(wrapper.vm.policy).toMatchObject({
name: 'test-policy',
description: 'test description',
isEnabled: false,
endpointMatchMode: EndpointMatchModeLabel,
endpointLabels: 'foo:bar',
rules: [
{
ruleType: RuleTypeEndpoint,
matchLabels: 'foo:bar',
},
],
labels: { 'app.gitlab.com/proj': '21' },
});
});
it('saves L7 policies', async () => {
factory({
data: () => ({
editorMode: EditorModeYAML,
yamlEditorValue: l7manifest,
}),
});
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: { manifest: l7manifest },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
});
describe('given there is a name change', () => {
let initialValue;
beforeEach(() => {
initialValue = findPreview().props('policyYaml');
findPolicyName().vm.$emit('input', 'new');
});
it('updates policy yaml preview', () => {
expect(findPreview().props('policyYaml')).not.toEqual(initialValue);
});
});
describe('given there is a rule change', () => {
let initialValue;
beforeEach(() => {
initialValue = findPreview().props('policyDescription');
wrapper.findByTestId('add-rule').vm.$emit('click');
});
it('updates policy description preview', () => {
expect(findPreview().props('policyDescription')).not.toEqual(initialValue);
});
});
it('adds a new rule', async () => {
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
const button = findAddRuleButton();
button.vm.$emit('click');
button.vm.$emit('click');
await wrapper.vm.$nextTick();
const elements = wrapper.findAllComponents(PolicyRuleBuilder);
expect(elements).toHaveLength(3);
elements.wrappers.forEach((builder, idx) => {
expect(builder.props().rule).toMatchObject({
ruleType: RuleTypeEndpoint,
direction: RuleDirectionInbound,
matchLabels: '',
portMatchMode: PortMatchModeAny,
ports: '',
});
expect(builder.props().endpointSelectorDisabled).toEqual(idx !== 0);
});
});
it('removes a new rule', async () => {
findAddRuleButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(2);
findPolicyRuleBuilder().vm.$emit('remove');
await wrapper.vm.$nextTick();
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
});
it('updates yaml editor value on switch to yaml editor', async () => {
findPolicyName().vm.$emit('input', 'test-policy');
findEditorModeToggle().vm.$emit('input', EditorModeYAML);
await wrapper.vm.$nextTick();
const editor = findNetworkPolicyEditor();
expect(editor.exists()).toBe(true);
expect(fromYaml(editor.attributes('value'))).toMatchObject({
name: 'test-policy',
});
});
describe('given there is a yaml parsing error', () => {
beforeEach(() => {
factory({
data: () => ({
yamlEditorError: {},
}),
});
describe('default', () => {
it('renders the environment picker', () => {
expect(findEnvironmentPicker().exists()).toBe(true);
});
it('disables policy name field', () => {
expect(findPolicyName().attributes().disabled).toBe('true');
});
it('disables policy description field', () => {
expect(findPolicyDescription().attributes().disabled).toBe('true');
});
it('disables policy enable/disable toggle', () => {
expect(findPolicyEnableContainer().attributes().disabled).toBe('true');
});
it('renders parsing error alert', () => {
expect(findYAMLParsingAlert().exists()).toBe(true);
});
it('disables rule builder', () => {
expect(wrapper.findByTestId('rule-builder-container').props().disabled).toBe(true);
});
it('disables action picker', () => {
expect(wrapper.findByTestId('policy-action-container').props().disabled).toBe(true);
});
it('disables policy preview', () => {
expect(wrapper.findByTestId('policy-preview-container').props().disabled).toBe(true);
});
it('does not update yaml editor value on switch to yaml editor', async () => {
findPolicyName().vm.$emit('input', 'test-policy');
findEditorModeToggle().vm.$emit('input', EditorModeYAML);
await wrapper.vm.$nextTick();
const editor = findNetworkPolicyEditor();
expect(editor.exists()).toBe(true);
expect(editor.attributes('value')).toEqual('');
});
});
it('creates policy and redirects to a threat monitoring path', async () => {
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: { manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
describe('given there is a createPolicy error', () => {
beforeEach(() => {
factory({
state: {
errorUpdatingPolicy: true,
},
});
});
it('it does not redirect', async () => {
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(redirectTo).not.toHaveBeenCalledWith('/threat-monitoring');
});
});
describe('given existingPolicy property was provided', () => {
const manifest = toYaml({
name: 'policy',
endpointLabels: '',
rules: [buildRule()],
});
beforeEach(() => {
factory({
propsData: {
existingPolicy: { name: 'policy', manifest },
},
});
});
it('presents existing policy', () => {
expect(findPolicyName().attributes().value).toEqual('policy');
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
});
it('updates existing policy and redirects to a threat monitoring path', async () => {
const saveButton = findSavePolicy();
expect(saveButton.text()).toEqual('Save changes');
saveButton.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/updatePolicy', {
environmentId: -1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
describe('given there is a updatePolicy error', () => {
beforeEach(() => {
factory({
propsData: {
existingPolicy: { name: 'policy', manifest },
},
state: {
errorUpdatingPolicy: true,
},
});
});
it('it does not redirect', async () => {
findSavePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(redirectTo).not.toHaveBeenCalledWith('/threat-monitoring');
});
});
it('renders delete button', () => {
expect(findDeletePolicy().exists()).toBe(true);
});
it('it does not trigger deletePolicy on delete button click', async () => {
findDeletePolicy().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).not.toHaveBeenCalledWith('networkPolicies/deletePolicy');
});
it('removes policy and redirects to a threat monitoring path on secondary modal button click', async () => {
wrapper.findComponent(GlModal).vm.$emit('secondary');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/deletePolicy', {
environmentId: -1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
});
});
describe('add alert picker', () => {
it('adds a policy annotation on alert addition', async () => {
await modifyPolicyAlert({ isAlertEnabled: true });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: {
manifest: expect.stringContaining("app.gitlab.com/alert: 'true'"),
},
});
it('renders the form select', () => {
const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true);
expect(formSelect.attributes('value')).toBe(POLICY_TYPES.networkPolicy.value);
});
it('removes a policy annotation on alert removal', async () => {
await modifyPolicyAlert({ isAlertEnabled: false });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
policy: {
manifest: expect.not.stringContaining("app.gitlab.com/alert: 'true'"),
},
});
it('renders the "NetworkPolicyEditor" component', () => {
expect(findNeworkPolicyEditor().exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import NetworkPolicyEditor from 'ee/threat_monitoring/components/network_policy_editor.vue';
import PolicyYamlEditor from 'ee/threat_monitoring/components/policy_yaml_editor.vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
describe('NetworkPolicyEditor component', () => {
describe('PolicyYamlEditor component', () => {
let wrapper;
const findEditor = () => wrapper.findComponent(EditorLite);
const factory = ({ propsData } = {}) => {
wrapper = shallowMount(NetworkPolicyEditor, {
wrapper = shallowMount(PolicyYamlEditor, {
propsData: {
value: 'foo',
...propsData,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment