Commit 378acdbf authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'network-policy-editor-preview' into 'master'

Network policy editor preview

See merge request gitlab-org/gitlab!39407
parents 7a9c83cd 1ba779f8
import { sprintf, __, s__ } from '~/locale';
import {
EndpointMatchModeAny,
RuleDirectionInbound,
PortMatchModeAny,
RuleTypeEntity,
RuleTypeCIDR,
RuleTypeFQDN,
} from '../constants';
import { portSelectors, labelSelector, splitItems } from './utils';
const strongArgs = { strongOpen: '<strong>', strongClose: '</strong>' };
/*
Return humanizied description for a port matcher of a rule.
*/
function humanizeNetworkPolicyRulePorts(rule) {
const { portMatchMode } = rule;
if (portMatchMode === PortMatchModeAny)
return sprintf(s__('NetworkPolicies|%{strongOpen}any%{strongClose} port'), strongArgs, false);
const portList = portSelectors(rule);
const ports = portList.map(({ port, protocol }) => `${port}/${protocol}`).join(', ');
return sprintf(
s__('NetworkPolicies|ports %{ports}'),
{
ports: `<strong>${ports}</strong>`,
},
false,
);
}
/*
Return humanizied description of an endpoint rule.
*/
function humanizeNetworkPolicyRuleEndpoint({ matchLabels }) {
const matchSelector = labelSelector(matchLabels);
const labels = Object.keys(matchSelector)
.map(key => `${key}: ${matchSelector[key]}`)
.join(', ');
return labels.length === 0
? sprintf(s__('NetworkPolicies|%{strongOpen}all%{strongClose} pods'), strongArgs, false)
: sprintf(
s__('NetworkPolicies|pods %{pods}'),
{
pods: `<strong>[${labels}]</strong>`,
},
false,
);
}
/*
Return humanizied description of an entity rule.
*/
function humanizeNetworkPolicyRuleEntity({ entities }) {
const entitiesList = entities.length === 0 ? s__('NetworkPolicies|nowhere') : entities.join(', ');
return `<strong>${entitiesList}</strong>`;
}
/*
Return humanizied description of a cidr rule.
*/
function humanizeNetworkPolicyRuleCIDR({ cidr }) {
const cidrList = splitItems(cidr);
const cidrs =
cidrList.length === 0 ? s__('NetworkPolicies|all IP addresses') : cidrList.join(', ');
return `<strong>${cidrs}</strong>`;
}
/*
Return humanizied description of a fqdn rule.
*/
function humanizeNetworkPolicyRuleFQDN({ fqdn }) {
const fqdnList = splitItems(fqdn);
const fqdns = fqdnList.length === 0 ? s__('NetworkPolicies|all DNS names') : fqdnList.join(', ');
return `<strong>${fqdns}</strong>`;
}
/*
Return humanizied description of a rule.
*/
function humanizeNetworkPolicyRule(rule) {
const { ruleType } = rule;
switch (ruleType) {
case RuleTypeEntity:
return humanizeNetworkPolicyRuleEntity(rule);
case RuleTypeCIDR:
return humanizeNetworkPolicyRuleCIDR(rule);
case RuleTypeFQDN:
return humanizeNetworkPolicyRuleFQDN(rule);
default:
return humanizeNetworkPolicyRuleEndpoint(rule);
}
}
/*
Return humanizied description of an endpoint matcher of a policy.
*/
function humanizeEndpointSelector({ endpointMatchMode, endpointLabels }) {
if (endpointMatchMode === EndpointMatchModeAny)
return sprintf(s__('NetworkPolicies|%{strongOpen}all%{strongClose} pods'), strongArgs, false);
const selector = labelSelector(endpointLabels);
const pods = Object.keys(selector)
.map(key => `${key}: ${selector[key]}`)
.join(', ');
return sprintf(
s__('NetworkPolicies|pods %{pods}'),
{
pods: `<strong>[${pods}]</strong>`,
},
false,
);
}
/*
Return humanizied description of a provided network policy.
*/
export default function humanizeNetworkPolicy(policy) {
const { rules } = policy;
if (rules.length === 0) return s__('NetworkPolicies|Deny all traffic');
const selector = humanizeEndpointSelector(policy);
const humanizedRules = rules.map(rule => {
const { direction } = rule;
const template =
direction === RuleDirectionInbound
? s__(
'NetworkPolicies|Allow all inbound traffic to %{selector} from %{ruleSelector} on %{ports}',
)
: s__(
'NetworkPolicies|Allow all outbound traffic from %{selector} to %{ruleSelector} on %{ports}',
);
const ruleSelector = humanizeNetworkPolicyRule(rule);
const ports = humanizeNetworkPolicyRulePorts(rule);
return sprintf(template, { selector, ruleSelector, ports }, false);
});
return humanizedRules.join(`<br><br>${__('and').toLocaleUpperCase()}<br><br>`);
}
......@@ -6,37 +6,23 @@ import {
RuleDirectionInbound,
PortMatchModeAny,
} from '../constants';
import { portSelectors, labelSelector, splitItems } from './utils';
/*
Return kubernetes specification object that is shared by all rule types.
*/
function commonSpec({ portMatchMode, ports }) {
if (portMatchMode === PortMatchModeAny) return {};
const portSelectors = ports.split(/\s/).reduce((acc, item) => {
const [port, protocol = 'tcp'] = item.split('/');
const portNumber = parseInt(port, 10);
if (Number.isNaN(portNumber)) return acc;
acc.push({ port, protocol: protocol.trim().toUpperCase() });
return acc;
}, []);
return { toPorts: [{ ports: portSelectors }] };
function commonSpec(rule) {
const spec = {};
const ports = portSelectors(rule);
if (Object.keys(ports).length > 0) spec.toPorts = [{ ports }];
return spec;
}
/*
Return kubernetes specification object for an endpoint rule.
*/
function ruleEndpointSpec({ direction, matchLabels }) {
const matchSelector = matchLabels.split(/\s/).reduce((acc, item) => {
const [key, value = ''] = item.split(':');
if (key.length === 0) return acc;
acc[key] = value.trim();
return acc;
}, {});
const matchSelector = labelSelector(matchLabels);
if (Object.keys(matchSelector).length === 0) return {};
return {
......@@ -63,7 +49,7 @@ function ruleEntitySpec({ direction, entities }) {
Return kubernetes specification object for a cidr rule.
*/
function ruleCIDRSpec({ direction, cidr }) {
const cidrList = cidr.length === 0 ? [] : cidr.split(/\s/);
const cidrList = splitItems(cidr);
if (cidrList.length === 0) return {};
return {
......@@ -77,7 +63,7 @@ function ruleCIDRSpec({ direction, cidr }) {
function ruleFQDNSpec({ direction, fqdn }) {
if (direction === RuleDirectionInbound) return {};
const fqdnList = fqdn.length === 0 ? [] : fqdn.split(/\s/);
const fqdnList = splitItems(fqdn);
if (fqdnList.length === 0) return {};
return {
......
import { safeDump } from 'js-yaml';
import { EndpointMatchModeAny } from '../constants';
import { ruleSpec } from './rules';
/*
Convert enpdoint labels provided as a string into a kubernetes selector.
Expected endpointLabels in format "one two:three"
*/
function endpointSelector({ endpointMatchMode, endpointLabels }) {
if (endpointMatchMode === EndpointMatchModeAny) return {};
return endpointLabels.split(/\s/).reduce((acc, item) => {
const [key, value = ''] = item.split(':');
if (key.length === 0) return acc;
acc[key] = value.trim();
return acc;
}, {});
}
import { labelSelector } from './utils';
import { EndpointMatchModeAny } from '../constants';
/*
Return kubernetes resource specification object for a policy.
*/
function spec(policy) {
const { description, rules, isEnabled } = policy;
const matchLabels = endpointSelector(policy);
function spec({ description, rules, isEnabled, endpointMatchMode, endpointLabels }) {
const matchLabels =
endpointMatchMode === EndpointMatchModeAny ? {} : labelSelector(endpointLabels);
const policySpec = {};
if (description?.length > 0) {
......
import { PortMatchModeAny } from '../constants';
/*
Convert space separated list of labels into a kubernetes selector.
Expects matchLabels in format "one two:three"
*/
export function labelSelector(labels) {
return labels.split(/\s/).reduce((acc, item) => {
const [key, value = ''] = item.split(':');
if (key.length === 0) return acc;
acc[key] = value.trim();
return acc;
}, {});
}
/*
Convert ports provided as a string into a kubernetes port selectors.
Expects ports in format "80/tcp 81"
*/
export function portSelectors({ portMatchMode, ports }) {
if (portMatchMode === PortMatchModeAny) return [];
return ports.split(/\s/).reduce((acc, item) => {
const [port, protocol = 'tcp'] = item.split('/');
const portNumber = parseInt(port, 10);
if (Number.isNaN(portNumber)) return acc;
acc.push({ port, protocol: protocol.trim().toUpperCase() });
return acc;
}, []);
}
/*
Convert whitespace separated list of items provided as a string into a list.
Expects items in format "0.0.0.0/24 1.1.1.1/32"
*/
export function splitItems(items) {
return items.split(/\s/).filter(item => item.length > 0);
}
......@@ -21,7 +21,9 @@ import {
EndpointMatchModeAny,
RuleTypeEndpoint,
} from './constants';
import toYaml from './lib/to_yaml';
import { buildRule } from './lib/rules';
import humanizeNetworkPolicy from './lib/humanize';
export default {
components: {
......@@ -52,6 +54,12 @@ export default {
};
},
computed: {
humanizedPolicy() {
return humanizeNetworkPolicy(this.policy);
},
policyYaml() {
return toYaml(this.policy);
},
shouldShowRuleEditor() {
return this.editorMode === EditorModeRule;
},
......@@ -163,7 +171,7 @@ export default {
</div>
<div class="col-sm-12 col-md-6 col-lg-5 col-xl-4">
<h5>{{ s__('NetworkPolicies|Policy preview') }}</h5>
<policy-preview />
<policy-preview :policy-yaml="policyYaml" :policy-description="humanizedPolicy" />
</div>
</div>
<div v-if="shouldShowYamlEditor" class="row" data-testid="yaml-editor">
......
<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>
<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>
......@@ -178,7 +178,18 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = `
Policy preview
</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>
......
// 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>
`;
import humanizeNetworkPolicy from 'ee/threat_monitoring/components/policy_editor/lib/humanize';
import { buildRule } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
import {
EndpointMatchModeAny,
EndpointMatchModeLabel,
PortMatchModePortProtocol,
RuleDirectionOutbound,
RuleTypeEntity,
RuleTypeCIDR,
RuleTypeFQDN,
} from 'ee/threat_monitoring/components/policy_editor/constants';
describe('humanizeNetworkPolicy', () => {
let policy;
let rule;
beforeEach(() => {
rule = buildRule();
policy = {
name: 'test-policy',
endpointMatchMode: EndpointMatchModeAny,
endpointLabels: '',
rules: [rule],
};
});
describe('without rules', () => {
beforeEach(() => {
policy.rules = [];
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual('Deny all traffic');
});
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to <strong>all</strong> pods ' +
'from <strong>all</strong> pods ' +
'on <strong>any</strong> port',
);
});
describe('with endpoint labels', () => {
beforeEach(() => {
policy.endpointMatchMode = EndpointMatchModeLabel;
policy.endpointLabels = 'one two:value two:another';
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to pods <strong>[one: , two: another]</strong> ' +
'from <strong>all</strong> pods ' +
'on <strong>any</strong> port',
);
});
});
describe('with additional egress rule', () => {
beforeEach(() => {
const anotherRule = buildRule();
anotherRule.direction = RuleDirectionOutbound;
policy.rules.push(anotherRule);
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to <strong>all</strong> pods from <strong>all</strong> pods on <strong>any</strong> port' +
'<br><br>AND<br><br>' +
'Allow all outbound traffic from <strong>all</strong> pods to <strong>all</strong> pods on <strong>any</strong> port',
);
});
});
describe('with ports', () => {
beforeEach(() => {
rule.portMatchMode = PortMatchModePortProtocol;
rule.ports = '80 81/udp invalid';
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to <strong>all</strong> pods ' +
'from <strong>all</strong> pods ' +
'on ports <strong>80/TCP, 81/UDP</strong>',
);
});
});
describe('with endpoint rule', () => {
beforeEach(() => {
rule.matchLabels = 'one two:value two:another';
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to <strong>all</strong> pods ' +
'from pods <strong>[one: , two: another]</strong> ' +
'on <strong>any</strong> port',
);
});
});
describe('with entity rule', () => {
beforeEach(() => {
rule = buildRule(RuleTypeEntity);
rule.entities = ['host', 'world'];
policy.rules = [rule];
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to <strong>all</strong> pods ' +
'from <strong>host, world</strong> ' +
'on <strong>any</strong> port',
);
});
});
describe('with cidr rule', () => {
beforeEach(() => {
rule = buildRule(RuleTypeCIDR);
rule.cidr = '0.0.0.0/32 1.1.1.1/24';
policy.rules = [rule];
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to <strong>all</strong> pods ' +
'from <strong>0.0.0.0/32, 1.1.1.1/24</strong> ' +
'on <strong>any</strong> port',
);
});
});
describe('with fqdn rule', () => {
beforeEach(() => {
rule = buildRule(RuleTypeFQDN);
rule.fqdn = 'some-service.com another-service.com';
policy.rules = [rule];
});
it('returns policy description', () => {
expect(humanizeNetworkPolicy(policy)).toEqual(
'Allow all inbound traffic to <strong>all</strong> pods ' +
'from <strong>some-service.com, another-service.com</strong> ' +
'on <strong>any</strong> port',
);
});
});
});
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 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 createStore from 'ee/threat_monitoring/store';
import {
......@@ -29,6 +30,7 @@ describe('PolicyEditorApp component', () => {
const findRuleEditor = () => wrapper.find('[data-testid="rule-editor"]');
const findYamlEditor = () => wrapper.find('[data-testid="yaml-editor"]');
const findPreview = () => wrapper.find(PolicyPreview);
beforeEach(() => {
factory();
......@@ -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 () => {
expect(wrapper.findAll(PolicyRuleBuilder).length).toEqual(0);
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();
});
});
......@@ -16155,6 +16155,15 @@ msgstr ""
msgid "NetworkPolicies|%{number} selected"
msgstr ""
msgid "NetworkPolicies|%{strongOpen}all%{strongClose} pods"
msgstr ""
msgid "NetworkPolicies|%{strongOpen}any%{strongClose} port"
msgstr ""
msgid "NetworkPolicies|.yaml"
msgstr ""
msgid "NetworkPolicies|.yaml mode"
msgstr ""
......@@ -16164,6 +16173,12 @@ msgstr ""
msgid "NetworkPolicies|All selected"
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."
msgstr ""
......@@ -16173,6 +16188,9 @@ msgstr ""
msgid "NetworkPolicies|Define this policy's location, conditions and actions."
msgstr ""
msgid "NetworkPolicies|Deny all traffic"
msgstr ""
msgid "NetworkPolicies|Description"
msgstr ""
......@@ -16242,6 +16260,9 @@ msgstr ""
msgid "NetworkPolicies|Policy type"
msgstr ""
msgid "NetworkPolicies|Rule"
msgstr ""
msgid "NetworkPolicies|Rule mode"
msgstr ""
......@@ -16260,6 +16281,12 @@ msgstr ""
msgid "NetworkPolicies|YAML editor"
msgstr ""
msgid "NetworkPolicies|all DNS names"
msgstr ""
msgid "NetworkPolicies|all IP addresses"
msgstr ""
msgid "NetworkPolicies|any pod"
msgstr ""
......@@ -16275,15 +16302,24 @@ msgstr ""
msgid "NetworkPolicies|inbound to"
msgstr ""
msgid "NetworkPolicies|nowhere"
msgstr ""
msgid "NetworkPolicies|outbound from"
msgstr ""
msgid "NetworkPolicies|pod with labels"
msgstr ""
msgid "NetworkPolicies|pods %{pods}"
msgstr ""
msgid "NetworkPolicies|pods with labels"
msgstr ""
msgid "NetworkPolicies|ports %{ports}"
msgstr ""
msgid "NetworkPolicies|ports/protocols"
msgstr ""
......@@ -28778,6 +28814,9 @@ msgstr ""
msgid "among other things"
msgstr ""
msgid "and"
msgstr ""
msgid "any-approver for the merge request already exists"
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