Commit 1ba779f8 authored by ap4y's avatar ap4y

Implement preview component for the policy editor

This commit implements yaml and humanized previews for policy editor
parent 2d04c2cd
...@@ -7,13 +7,7 @@ import { ...@@ -7,13 +7,7 @@ import {
RuleTypeCIDR, RuleTypeCIDR,
RuleTypeFQDN, RuleTypeFQDN,
} from '../constants'; } from '../constants';
import { import { portSelectors, labelSelector, splitItems } from './utils';
endpointSelector,
portSelectors,
ruleEndpointSelector,
ruleCIDRList,
ruleFQDNList,
} from './utils';
const strongArgs = { strongOpen: '<strong>', strongClose: '</strong>' }; const strongArgs = { strongOpen: '<strong>', strongClose: '</strong>' };
...@@ -41,7 +35,7 @@ function humanizeNetworkPolicyRulePorts(rule) { ...@@ -41,7 +35,7 @@ function humanizeNetworkPolicyRulePorts(rule) {
Return humanizied description of an endpoint rule. Return humanizied description of an endpoint rule.
*/ */
function humanizeNetworkPolicyRuleEndpoint({ matchLabels }) { function humanizeNetworkPolicyRuleEndpoint({ matchLabels }) {
const matchSelector = ruleEndpointSelector(matchLabels); const matchSelector = labelSelector(matchLabels);
const labels = Object.keys(matchSelector) const labels = Object.keys(matchSelector)
.map(key => `${key}: ${matchSelector[key]}`) .map(key => `${key}: ${matchSelector[key]}`)
.join(', '); .join(', ');
...@@ -68,7 +62,7 @@ function humanizeNetworkPolicyRuleEntity({ entities }) { ...@@ -68,7 +62,7 @@ function humanizeNetworkPolicyRuleEntity({ entities }) {
Return humanizied description of a cidr rule. Return humanizied description of a cidr rule.
*/ */
function humanizeNetworkPolicyRuleCIDR({ cidr }) { function humanizeNetworkPolicyRuleCIDR({ cidr }) {
const cidrList = ruleCIDRList(cidr); const cidrList = splitItems(cidr);
const cidrs = const cidrs =
cidrList.length === 0 ? s__('NetworkPolicies|all IP addresses') : cidrList.join(', '); cidrList.length === 0 ? s__('NetworkPolicies|all IP addresses') : cidrList.join(', ');
return `<strong>${cidrs}</strong>`; return `<strong>${cidrs}</strong>`;
...@@ -78,7 +72,7 @@ function humanizeNetworkPolicyRuleCIDR({ cidr }) { ...@@ -78,7 +72,7 @@ function humanizeNetworkPolicyRuleCIDR({ cidr }) {
Return humanizied description of a fqdn rule. Return humanizied description of a fqdn rule.
*/ */
function humanizeNetworkPolicyRuleFQDN({ fqdn }) { function humanizeNetworkPolicyRuleFQDN({ fqdn }) {
const fqdnList = ruleFQDNList(fqdn); const fqdnList = splitItems(fqdn);
const fqdns = fqdnList.length === 0 ? s__('NetworkPolicies|all DNS names') : fqdnList.join(', '); const fqdns = fqdnList.length === 0 ? s__('NetworkPolicies|all DNS names') : fqdnList.join(', ');
return `<strong>${fqdns}</strong>`; return `<strong>${fqdns}</strong>`;
} }
...@@ -104,12 +98,11 @@ function humanizeNetworkPolicyRule(rule) { ...@@ -104,12 +98,11 @@ function humanizeNetworkPolicyRule(rule) {
/* /*
Return humanizied description of an endpoint matcher of a policy. Return humanizied description of an endpoint matcher of a policy.
*/ */
function humanizeEndpointSelector(policy) { function humanizeEndpointSelector({ endpointMatchMode, endpointLabels }) {
const { endpointMatchMode } = policy;
if (endpointMatchMode === EndpointMatchModeAny) if (endpointMatchMode === EndpointMatchModeAny)
return sprintf(s__('NetworkPolicies|%{strongOpen}all%{strongClose} pods'), strongArgs, false); return sprintf(s__('NetworkPolicies|%{strongOpen}all%{strongClose} pods'), strongArgs, false);
const selector = endpointSelector(policy); const selector = labelSelector(endpointLabels);
const pods = Object.keys(selector) const pods = Object.keys(selector)
.map(key => `${key}: ${selector[key]}`) .map(key => `${key}: ${selector[key]}`)
.join(', '); .join(', ');
...@@ -146,5 +139,5 @@ export default function humanizeNetworkPolicy(policy) { ...@@ -146,5 +139,5 @@ export default function humanizeNetworkPolicy(policy) {
return sprintf(template, { selector, ruleSelector, ports }, false); return sprintf(template, { selector, ruleSelector, ports }, false);
}); });
return humanizedRules.join(`<br><br>${__('and').toUpperCase()}<br><br>`); return humanizedRules.join(`<br><br>${__('and').toLocaleUpperCase()}<br><br>`);
} }
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
RuleDirectionInbound, RuleDirectionInbound,
PortMatchModeAny, PortMatchModeAny,
} from '../constants'; } from '../constants';
import { portSelectors, ruleEndpointSelector, ruleCIDRList, ruleFQDNList } from './utils'; import { portSelectors, labelSelector, splitItems } from './utils';
/* /*
Return kubernetes specification object that is shared by all rule types. Return kubernetes specification object that is shared by all rule types.
...@@ -22,7 +22,7 @@ function commonSpec(rule) { ...@@ -22,7 +22,7 @@ function commonSpec(rule) {
Return kubernetes specification object for an endpoint rule. Return kubernetes specification object for an endpoint rule.
*/ */
function ruleEndpointSpec({ direction, matchLabels }) { function ruleEndpointSpec({ direction, matchLabels }) {
const matchSelector = ruleEndpointSelector(matchLabels); const matchSelector = labelSelector(matchLabels);
if (Object.keys(matchSelector).length === 0) return {}; if (Object.keys(matchSelector).length === 0) return {};
return { return {
...@@ -49,7 +49,7 @@ function ruleEntitySpec({ direction, entities }) { ...@@ -49,7 +49,7 @@ function ruleEntitySpec({ direction, entities }) {
Return kubernetes specification object for a cidr rule. Return kubernetes specification object for a cidr rule.
*/ */
function ruleCIDRSpec({ direction, cidr }) { function ruleCIDRSpec({ direction, cidr }) {
const cidrList = ruleCIDRList(cidr); const cidrList = splitItems(cidr);
if (cidrList.length === 0) return {}; if (cidrList.length === 0) return {};
return { return {
...@@ -63,7 +63,7 @@ function ruleCIDRSpec({ direction, cidr }) { ...@@ -63,7 +63,7 @@ function ruleCIDRSpec({ direction, cidr }) {
function ruleFQDNSpec({ direction, fqdn }) { function ruleFQDNSpec({ direction, fqdn }) {
if (direction === RuleDirectionInbound) return {}; if (direction === RuleDirectionInbound) return {};
const fqdnList = ruleFQDNList(fqdn); const fqdnList = splitItems(fqdn);
if (fqdnList.length === 0) return {}; if (fqdnList.length === 0) return {};
return { return {
......
import { safeDump } from 'js-yaml'; import { safeDump } from 'js-yaml';
import { ruleSpec } from './rules'; import { ruleSpec } from './rules';
import { endpointSelector } from './utils'; import { labelSelector } from './utils';
import { EndpointMatchModeAny } from '../constants';
/* /*
Return kubernetes resource specification object for a policy. Return kubernetes resource specification object for a policy.
*/ */
function spec(policy) { function spec({ description, rules, isEnabled, endpointMatchMode, endpointLabels }) {
const { description, rules, isEnabled } = policy; const matchLabels =
const matchLabels = endpointSelector(policy); endpointMatchMode === EndpointMatchModeAny ? {} : labelSelector(endpointLabels);
const policySpec = {}; const policySpec = {};
if (description?.length > 0) { if (description?.length > 0) {
......
import { EndpointMatchModeAny, PortMatchModeAny } from '../constants'; import { PortMatchModeAny } from '../constants';
/* /*
Convert enpdoint labels provided as a string into a kubernetes selector. Convert space separated list of labels into a kubernetes selector.
Expects endpointLabels in format "one two:three" Expects matchLabels in format "one two:three"
*/ */
export function endpointSelector({ endpointMatchMode, endpointLabels }) { export function labelSelector(labels) {
if (endpointMatchMode === EndpointMatchModeAny) return {}; return labels.split(/\s/).reduce((acc, item) => {
return endpointLabels.split(/\s/).reduce((acc, item) => {
const [key, value = ''] = item.split(':'); const [key, value = ''] = item.split(':');
if (key.length === 0) return acc; if (key.length === 0) return acc;
...@@ -21,7 +19,7 @@ export function endpointSelector({ endpointMatchMode, endpointLabels }) { ...@@ -21,7 +19,7 @@ export function endpointSelector({ endpointMatchMode, endpointLabels }) {
Expects ports in format "80/tcp 81" Expects ports in format "80/tcp 81"
*/ */
export function portSelectors({ portMatchMode, ports }) { export function portSelectors({ portMatchMode, ports }) {
if (portMatchMode === PortMatchModeAny) return {}; if (portMatchMode === PortMatchModeAny) return [];
return ports.split(/\s/).reduce((acc, item) => { return ports.split(/\s/).reduce((acc, item) => {
const [port, protocol = 'tcp'] = item.split('/'); const [port, protocol = 'tcp'] = item.split('/');
...@@ -34,31 +32,9 @@ export function portSelectors({ portMatchMode, ports }) { ...@@ -34,31 +32,9 @@ export function portSelectors({ portMatchMode, ports }) {
} }
/* /*
Convert list of labels provided as a string into a kubernetes endpoint selector. Convert whitespace separated list of items provided as a string into a list.
Expects matchLabels in format "one two:three" Expects items in format "0.0.0.0/24 1.1.1.1/32"
*/
export function ruleEndpointSelector(matchLabels) {
return matchLabels.split(/\s/).reduce((acc, item) => {
const [key, value = ''] = item.split(':');
if (key.length === 0) return acc;
acc[key] = value.trim();
return acc;
}, {});
}
/*
Convert list of CIDRs provided as a string into a CIDR list expected by the kubernetes policy.
Expects cidr in format "0.0.0.0/24 1.1.1.1/32"
*/
export function ruleCIDRList(cidr) {
return cidr.length === 0 ? [] : cidr.split(/\s/);
}
/*
Convert list of FQDNs provided as a string into a FQDN list expected be the kubernetes policy.
Expects fqdn in format "one-service.com another-service.com"
*/ */
export function ruleFQDNList(fqdn) { export function splitItems(items) {
return fqdn.length === 0 ? [] : fqdn.split(/\s/); return items.split(/\s/).filter(item => item.length > 0);
} }
...@@ -21,7 +21,9 @@ import { ...@@ -21,7 +21,9 @@ import {
EndpointMatchModeAny, EndpointMatchModeAny,
RuleTypeEndpoint, RuleTypeEndpoint,
} from './constants'; } from './constants';
import toYaml from './lib/to_yaml';
import { buildRule } from './lib/rules'; import { buildRule } from './lib/rules';
import humanizeNetworkPolicy from './lib/humanize';
export default { export default {
components: { components: {
...@@ -52,6 +54,12 @@ export default { ...@@ -52,6 +54,12 @@ export default {
}; };
}, },
computed: { computed: {
humanizedPolicy() {
return humanizeNetworkPolicy(this.policy);
},
policyYaml() {
return toYaml(this.policy);
},
shouldShowRuleEditor() { shouldShowRuleEditor() {
return this.editorMode === EditorModeRule; return this.editorMode === EditorModeRule;
}, },
...@@ -163,7 +171,7 @@ export default { ...@@ -163,7 +171,7 @@ export default {
</div> </div>
<div class="col-sm-12 col-md-6 col-lg-5 col-xl-4"> <div class="col-sm-12 col-md-6 col-lg-5 col-xl-4">
<h5>{{ s__('NetworkPolicies|Policy preview') }}</h5> <h5>{{ s__('NetworkPolicies|Policy preview') }}</h5>
<policy-preview /> <policy-preview :policy-yaml="policyYaml" :policy-description="humanizedPolicy" />
</div> </div>
</div> </div>
<div v-if="shouldShowYamlEditor" class="row" data-testid="yaml-editor"> <div v-if="shouldShowYamlEditor" class="row" data-testid="yaml-editor">
......
<script> <script>
export default {}; import { GlTabs, GlTab, GlSafeHtmlDirective } from '@gitlab/ui';
export default {
components: {
GlTabs,
GlTab,
},
directives: {
safeHtml: GlSafeHtmlDirective,
},
props: {
policyYaml: {
type: String,
required: true,
},
policyDescription: {
type: String,
required: true,
},
},
safeHtmlConfig: { ALLOWED_TAGS: ['strong', 'br'] },
};
</script> </script>
<template> <template>
<div class="gl-bg-gray-100 p-9"></div> <gl-tabs content-class="gl-pt-0">
<gl-tab :title="s__('NetworkPolicies|.yaml')">
<pre class="gl-bg-white gl-rounded-top-left-none gl-rounded-top-right-none">{{
policyYaml
}}</pre>
</gl-tab>
<gl-tab :title="s__('NetworkPolicies|Rule')">
<div
v-safe-html:[$options.safeHtmlConfig]="policyDescription"
class="gl-bg-white gl-rounded-top-left-none gl-rounded-top-right-none gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-py-3 gl-px-4 gl-border-1 gl-border-solid gl-border-gray-100"
></div>
</gl-tab>
</gl-tabs>
</template> </template>
...@@ -178,7 +178,18 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = ` ...@@ -178,7 +178,18 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = `
Policy preview Policy preview
</h5> </h5>
<policy-preview-stub /> <policy-preview-stub
policydescription="Deny all traffic"
policyyaml="apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: ''
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
"
/>
</div> </div>
</div> </div>
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PolicyPreview component renders policy preview tabs 1`] = `
<gl-tabs-stub
contentclass="gl-pt-0"
theme="indigo"
>
<gl-tab-stub
title=".yaml"
>
<pre
class="gl-bg-white gl-rounded-top-left-none gl-rounded-top-right-none"
>
foo
</pre>
</gl-tab-stub>
<gl-tab-stub
title="Rule"
>
<div
class="gl-bg-white gl-rounded-top-left-none gl-rounded-top-right-none gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-py-3 gl-px-4 gl-border-1 gl-border-solid gl-border-gray-100"
>
<strong>
bar
</strong>
<br />
test
</div>
</gl-tab-stub>
</gl-tabs-stub>
`;
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
RuleTypeFQDN, RuleTypeFQDN,
} from 'ee/threat_monitoring/components/policy_editor/constants'; } from 'ee/threat_monitoring/components/policy_editor/constants';
describe('humatnizeNetworkPolicy', () => { describe('humanizeNetworkPolicy', () => {
let policy; let policy;
let rule; let rule;
......
import {
labelSelector,
portSelectors,
splitItems,
} from 'ee/threat_monitoring/components/policy_editor/lib/utils';
import {
PortMatchModeAny,
PortMatchModePortProtocol,
} from 'ee/threat_monitoring/components/policy_editor/constants';
describe('labelSelector', () => {
it('returns selector map', () => {
expect(labelSelector('one two: three:value three:override ')).toMatchObject({
one: '',
two: '',
three: 'override',
});
});
});
describe('portSelectors', () => {
it('returns list of selectors', () => {
expect(
portSelectors({
portMatchMode: PortMatchModePortProtocol,
ports: '80 81/tcp 82/UDP ',
}),
).toEqual([
{ port: '80', protocol: 'TCP' },
{ port: '81', protocol: 'TCP' },
{ port: '82', protocol: 'UDP' },
]);
});
describe('when port match mode is any', () => {
it('returns empty selector', () => {
expect(
portSelectors({
portMatchMode: PortMatchModeAny,
ports: '80 81/tcp 81/UDP ',
}),
).toEqual([]);
});
});
});
describe('splitItems', () => {
it('returns list of entries', () => {
expect(splitItems('10.0.0.1/32 10.0.0.1/24 ')).toEqual(['10.0.0.1/32', '10.0.0.1/24']);
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PolicyEditorApp from 'ee/threat_monitoring/components/policy_editor/policy_editor.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 PolicyRuleBuilder from 'ee/threat_monitoring/components/policy_editor/policy_rule_builder.vue';
import createStore from 'ee/threat_monitoring/store'; import createStore from 'ee/threat_monitoring/store';
import { import {
...@@ -29,6 +30,7 @@ describe('PolicyEditorApp component', () => { ...@@ -29,6 +30,7 @@ describe('PolicyEditorApp component', () => {
const findRuleEditor = () => wrapper.find('[data-testid="rule-editor"]'); const findRuleEditor = () => wrapper.find('[data-testid="rule-editor"]');
const findYamlEditor = () => wrapper.find('[data-testid="yaml-editor"]'); const findYamlEditor = () => wrapper.find('[data-testid="yaml-editor"]');
const findPreview = () => wrapper.find(PolicyPreview);
beforeEach(() => { beforeEach(() => {
factory(); factory();
...@@ -67,6 +69,32 @@ describe('PolicyEditorApp component', () => { ...@@ -67,6 +69,32 @@ describe('PolicyEditorApp component', () => {
}); });
}); });
describe('given there is a name change', () => {
let initialValue;
beforeEach(() => {
initialValue = findPreview().props('policyYaml');
wrapper.find("[id='policyName']").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.find("[data-testid='add-rule']").vm.$emit('click');
});
it('updates policy description preview', () => {
expect(findPreview().props('policyDescription')).not.toEqual(initialValue);
});
});
it('adds a new rule', async () => { it('adds a new rule', async () => {
expect(wrapper.findAll(PolicyRuleBuilder).length).toEqual(0); expect(wrapper.findAll(PolicyRuleBuilder).length).toEqual(0);
const button = wrapper.find("[data-testid='add-rule']"); const button = wrapper.find("[data-testid='add-rule']");
......
import { shallowMount } from '@vue/test-utils';
import PolicyPreview from 'ee/threat_monitoring/components/policy_editor/policy_preview.vue';
import { GlTabs } from '@gitlab/ui';
describe('PolicyPreview component', () => {
let wrapper;
const factory = ({ propsData } = {}) => {
wrapper = shallowMount(PolicyPreview, {
propsData: {
...propsData,
},
});
};
beforeEach(() => {
factory({
propsData: {
policyYaml: 'foo',
policyDescription: '<strong>bar</strong><br><div>test</div><script></script>',
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders policy preview tabs', () => {
expect(wrapper.find(GlTabs).element).toMatchSnapshot();
});
});
...@@ -16131,6 +16131,15 @@ msgstr "" ...@@ -16131,6 +16131,15 @@ msgstr ""
msgid "NetworkPolicies|%{number} selected" msgid "NetworkPolicies|%{number} selected"
msgstr "" msgstr ""
msgid "NetworkPolicies|%{strongOpen}all%{strongClose} pods"
msgstr ""
msgid "NetworkPolicies|%{strongOpen}any%{strongClose} port"
msgstr ""
msgid "NetworkPolicies|.yaml"
msgstr ""
msgid "NetworkPolicies|.yaml mode" msgid "NetworkPolicies|.yaml mode"
msgstr "" msgstr ""
...@@ -16140,6 +16149,12 @@ msgstr "" ...@@ -16140,6 +16149,12 @@ msgstr ""
msgid "NetworkPolicies|All selected" msgid "NetworkPolicies|All selected"
msgstr "" msgstr ""
msgid "NetworkPolicies|Allow all inbound traffic to %{selector} from %{ruleSelector} on %{ports}"
msgstr ""
msgid "NetworkPolicies|Allow all outbound traffic from %{selector} to %{ruleSelector} on %{ports}"
msgstr ""
msgid "NetworkPolicies|Choose whether to enforce this policy." msgid "NetworkPolicies|Choose whether to enforce this policy."
msgstr "" msgstr ""
...@@ -16149,6 +16164,9 @@ msgstr "" ...@@ -16149,6 +16164,9 @@ msgstr ""
msgid "NetworkPolicies|Define this policy's location, conditions and actions." msgid "NetworkPolicies|Define this policy's location, conditions and actions."
msgstr "" msgstr ""
msgid "NetworkPolicies|Deny all traffic"
msgstr ""
msgid "NetworkPolicies|Description" msgid "NetworkPolicies|Description"
msgstr "" msgstr ""
...@@ -16218,6 +16236,9 @@ msgstr "" ...@@ -16218,6 +16236,9 @@ msgstr ""
msgid "NetworkPolicies|Policy type" msgid "NetworkPolicies|Policy type"
msgstr "" msgstr ""
msgid "NetworkPolicies|Rule"
msgstr ""
msgid "NetworkPolicies|Rule mode" msgid "NetworkPolicies|Rule mode"
msgstr "" msgstr ""
...@@ -16236,6 +16257,12 @@ msgstr "" ...@@ -16236,6 +16257,12 @@ msgstr ""
msgid "NetworkPolicies|YAML editor" msgid "NetworkPolicies|YAML editor"
msgstr "" msgstr ""
msgid "NetworkPolicies|all DNS names"
msgstr ""
msgid "NetworkPolicies|all IP addresses"
msgstr ""
msgid "NetworkPolicies|any pod" msgid "NetworkPolicies|any pod"
msgstr "" msgstr ""
...@@ -16251,15 +16278,24 @@ msgstr "" ...@@ -16251,15 +16278,24 @@ msgstr ""
msgid "NetworkPolicies|inbound to" msgid "NetworkPolicies|inbound to"
msgstr "" msgstr ""
msgid "NetworkPolicies|nowhere"
msgstr ""
msgid "NetworkPolicies|outbound from" msgid "NetworkPolicies|outbound from"
msgstr "" msgstr ""
msgid "NetworkPolicies|pod with labels" msgid "NetworkPolicies|pod with labels"
msgstr "" msgstr ""
msgid "NetworkPolicies|pods %{pods}"
msgstr ""
msgid "NetworkPolicies|pods with labels" msgid "NetworkPolicies|pods with labels"
msgstr "" msgstr ""
msgid "NetworkPolicies|ports %{ports}"
msgstr ""
msgid "NetworkPolicies|ports/protocols" msgid "NetworkPolicies|ports/protocols"
msgstr "" msgstr ""
...@@ -28751,6 +28787,9 @@ msgstr "" ...@@ -28751,6 +28787,9 @@ msgstr ""
msgid "among other things" msgid "among other things"
msgstr "" msgstr ""
msgid "and"
msgstr ""
msgid "any-approver for the merge request already exists" msgid "any-approver for the merge request already exists"
msgstr "" msgstr ""
......
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