Commit 3a6f0e28 authored by ap4y's avatar ap4y

Support yaml loading in the policy editor

This commit adds support for yaml editing in policy editor. YAML can
be updated in the related tab using a editor component (uses monaco
editor) and this will result in policy data changes.
parent 037f2a29
......@@ -7,6 +7,16 @@ export default {
type: String,
required: true,
},
readOnly: {
type: Boolean,
required: false,
default: true,
},
height: {
type: Number,
required: false,
default: 300,
},
},
data() {
return { editor: null };
......@@ -42,7 +52,7 @@ export default {
occurrencesHighlight: false,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
readOnly: true,
readOnly: this.readOnly,
});
this.editor.onDidChangeModelContent(() => {
this.$emit('input', this.editor.getValue());
......@@ -53,8 +63,5 @@ export default {
</script>
<template>
<div
ref="editor"
class="multi-file-editor-holer network-policy-editor gl-bg-gray-50 p-2 gl-overflow-x-hidden"
></div>
<div ref="editor" class="gl-overflow-hidden" :style="{ height: `${height}px` }"></div>
</template>
......@@ -235,7 +235,13 @@ export default {
<div v-if="hasSelectedPolicy">
<h5>{{ s__('NetworkPolicies|Policy definition') }}</h5>
<p>{{ s__("NetworkPolicies|Define this policy's location, conditions and actions.") }}</p>
<network-policy-editor ref="policyEditor" v-model="selectedPolicy.manifest" />
<div class="gl-p-3 gl-bg-gray-50">
<network-policy-editor
ref="policyEditor"
v-model="selectedPolicy.manifest"
class="network-policy-editor"
/>
</div>
<h5 class="mt-4">{{ s__('NetworkPolicies|Enforcement status') }}</h5>
<p>{{ s__('NetworkPolicies|Choose whether to enforce this policy.') }}</p>
......
......@@ -14,28 +14,54 @@ import {
RuleTypeFQDN,
} from '../constants';
/*
Convert list of matchLabel selectors used by the endpoint rule to an
entity rule object expected by the rule builder.
We expect list of object in format:
[{ matchLabels: { foo: 'bar' } }, { matchLabels: { bar: 'baz' } }]
And will return a single rule object:
{ matchLabels: 'foo:bar baz:bar' }
*/
function ruleTypeEndpointFunc(items) {
const labels = items
.reduce(
(acc, { matchLabels }) =>
acc.concat(Object.keys(matchLabels).map(key => `${key}:${matchLabels[key]}`)),
[],
)
.join(' ');
return { matchLabels: labels };
}
function ruleTypeEntityFunc(entities) {
return { entities };
}
function ruleTypeCIDRFunc(items) {
const cidr = items.join(' ');
return { cidr };
}
/*
Convert list of matchName selectors used by the fqdn rule to a
fqdn rule object expected by the rule builder.
We expect list of object in format:
[{ matchName: 'remote-service.com' }, { matchName: 'another-service.com' }]
And will return a single rule object:
{ fqdn: 'remote-service.com another-service.com' }
*/
function ruleTypeFQDNFunc(items) {
const fqdn = items.map(({ matchName }) => matchName).join(' ');
return { fqdn };
}
const rulesFunc = {
[RuleTypeEndpoint](items) {
const labels = items
.reduce(
(acc, { matchLabels }) =>
acc.concat(Object.keys(matchLabels).map(key => `${key}:${matchLabels[key]}`)),
[],
)
.join(' ');
return { matchLabels: labels };
},
[RuleTypeEntity](entities) {
return { entities };
},
[RuleTypeCIDR](items) {
const cidr = items.join(' ');
return { cidr };
},
[RuleTypeFQDN](items) {
const fqdn = items.map(({ matchName }) => matchName).join(' ');
return { fqdn };
},
[RuleTypeEndpoint]: ruleTypeEndpointFunc,
[RuleTypeEntity]: ruleTypeEntityFunc,
[RuleTypeCIDR]: ruleTypeCIDRFunc,
[RuleTypeFQDN]: ruleTypeFQDNFunc,
};
/*
......@@ -81,7 +107,9 @@ function parseRule(item, direction) {
}
/*
Construct a policy object expected by the policy editor from a yaml manifest
Construct a policy object expected by the policy editor from a yaml manifest.
Expected yaml structure is defined in the official documentation:
https://docs.cilium.io/en/v1.8/policy/language
*/
export default function fromYaml(manifest) {
const { metadata, spec } = safeLoad(manifest, { json: true });
......
......@@ -8,6 +8,7 @@ import {
GlToggle,
GlSegmentedControl,
GlButton,
GlAlert,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import EnvironmentPicker from '../environment_picker.vue';
......@@ -22,6 +23,7 @@ import {
RuleTypeEndpoint,
} from './constants';
import toYaml from './lib/to_yaml';
import fromYaml from './lib/from_yaml';
import { buildRule } from './lib/rules';
import humanizeNetworkPolicy from './lib/humanize';
......@@ -34,6 +36,7 @@ export default {
GlToggle,
GlSegmentedControl,
GlButton,
GlAlert,
EnvironmentPicker,
NetworkPolicyEditor,
PolicyRuleBuilder,
......@@ -43,6 +46,8 @@ export default {
data() {
return {
editorMode: EditorModeRule,
yamlEditorValue: '',
yamlEditorError: null,
policy: {
name: '',
description: '',
......@@ -66,6 +71,9 @@ export default {
shouldShowYamlEditor() {
return this.editorMode === EditorModeYAML;
},
hasParsingError() {
return Boolean(this.yamlEditorError);
},
},
created() {
this.fetchEnvironments();
......@@ -85,12 +93,32 @@ export default {
const rule = this.policy.rules[ruleIdx];
this.policy.rules.splice(ruleIdx, 1, buildRule(ruleType, rule));
},
loadYaml(manifest) {
this.yamlEditorValue = manifest;
this.yamlEditorError = null;
try {
Object.assign(this.policy, fromYaml(manifest));
} catch (error) {
this.yamlEditorError = error;
}
},
changeEditorMode(mode) {
if (mode === EditorModeYAML) {
this.yamlEditorValue = toYaml(this.policy);
}
this.editorMode = mode;
},
},
policyTypes: [{ value: 'networkPolicy', text: s__('NetworkPolicies|Network Policy') }],
editorModes: [
{ value: EditorModeRule, text: s__('NetworkPolicies|Rule mode') },
{ value: EditorModeYAML, text: s__('NetworkPolicies|.yaml mode') },
],
parsingErrorMessage: s__(
'NetworkPolicies|Rule mode is unavailable for this policy. In some cases, we cannot parse the YAML file back into the rules editor.',
),
};
</script>
......@@ -139,13 +167,22 @@ export default {
<div class="row">
<div class="col-md-auto">
<gl-form-group :label="s__('NetworkPolicies|Editor mode')" label-for="editorMode">
<gl-segmented-control v-model="editorMode" :options="$options.editorModes" />
<gl-segmented-control
data-testid="editor-mode"
:options="$options.editorModes"
:checked="editorMode"
@input="changeEditorMode"
/>
</gl-form-group>
</div>
</div>
<hr />
<div v-if="shouldShowRuleEditor" class="row" data-testid="rule-editor">
<div class="col-sm-12 col-md-6 col-lg-7 col-xl-8">
<gl-alert v-if="hasParsingError" data-testid="parsing-alert" :dismissible="false">{{
$options.parsingErrorMessage
}}</gl-alert>
<h4>{{ s__('NetworkPolicies|Rules') }}</h4>
<policy-rule-builder
v-for="(rule, idx) in policy.rules"
......@@ -161,9 +198,14 @@ export default {
/>
<div class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100">
<gl-button variant="link" category="primary" data-testid="add-rule" @click="addRule">{{
s__('Network Policy|New rule')
}}</gl-button>
<gl-button
variant="link"
category="primary"
data-testid="add-rule"
:disabled="hasParsingError"
@click="addRule"
>{{ s__('Network Policy|New rule') }}</gl-button
>
</div>
<h4>{{ s__('NetworkPolicies|Actions') }}</h4>
......@@ -177,10 +219,19 @@ export default {
<div v-if="shouldShowYamlEditor" class="row" data-testid="yaml-editor">
<div class="col-sm-12 col-md-12 col-lg-10 col-xl-8">
<div class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100">
<h5 class="gl-m-0 gl-p-3 gl-bg-gray-10 gl-border-b-gray-100">
<h5
class="gl-m-0 gl-p-4 gl-bg-gray-10 gl-border-1 gl-border-b-solid gl-border-b-gray-100"
>
{{ s__('NetworkPolicies|YAML editor') }}
</h5>
<network-policy-editor id="yamlEditor" value="" />
<div class="gl-p-4">
<network-policy-editor
:value="yamlEditorValue"
:height="400"
:read-only="false"
@input="loadYaml"
/>
</div>
</div>
</div>
</div>
......
.network-policy-editor {
min-height: 300px;
.monaco-editor,
.monaco-editor-background,
.monaco-editor .inputarea.ime-input {
......
......@@ -12,17 +12,21 @@ exports[`PolicyEditorApp component given .yaml editor mode is enabled renders ya
class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"
>
<h5
class="gl-m-0 gl-p-3 gl-bg-gray-10 gl-border-b-gray-100"
class="gl-m-0 gl-p-4 gl-bg-gray-10 gl-border-1 gl-border-b-solid gl-border-b-gray-100"
>
YAML editor
</h5>
<network-policy-editor-stub
id="yamlEditor"
value=""
/>
<div
class="gl-p-4"
>
<network-policy-editor-stub
height="400"
value=""
/>
</div>
</div>
</div>
</div>
......@@ -131,6 +135,7 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = `
>
<gl-segmented-control-stub
checked="rule"
data-testid="editor-mode"
options="[object Object],[object Object]"
/>
</gl-form-group-stub>
......@@ -146,6 +151,8 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = `
<div
class="col-sm-12 col-md-6 col-lg-7 col-xl-8"
>
<!---->
<h4>
Rules
</h4>
......
......@@ -2,12 +2,16 @@ 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 NetworkPolicyEditor from 'ee/threat_monitoring/components/network_policy_editor.vue';
import createStore from 'ee/threat_monitoring/store';
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';
describe('PolicyEditorApp component', () => {
let store;
......@@ -31,6 +35,9 @@ describe('PolicyEditorApp component', () => {
const findRuleEditor = () => wrapper.find('[data-testid="rule-editor"]');
const findYamlEditor = () => wrapper.find('[data-testid="yaml-editor"]');
const findPreview = () => wrapper.find(PolicyPreview);
const findAddRuleButton = () => wrapper.find('[data-testid="add-rule"]');
const findYAMLParsingAlert = () => wrapper.find('[data-testid="parsing-alert"]');
const findNetworkPolicyEditor = () => wrapper.find(NetworkPolicyEditor);
beforeEach(() => {
factory();
......@@ -49,11 +56,15 @@ describe('PolicyEditorApp component', () => {
expect(findYamlEditor().exists()).toBe(false);
});
it('does not render parsing error alert', () => {
expect(findYAMLParsingAlert().exists()).toBe(false);
});
describe('given .yaml editor mode is enabled', () => {
beforeEach(() => {
factory({
data: () => ({
editorMode: 'yaml',
editorMode: EditorModeYAML,
}),
});
});
......@@ -67,6 +78,38 @@ describe('PolicyEditorApp component', () => {
expect(editor.exists()).toBe(true);
expect(editor.element).toMatchSnapshot();
});
it('updates policy on yaml editor value change', async () => {
const manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
spec:
description: test description
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',
},
],
});
});
});
describe('given there is a name change', () => {
......@@ -97,7 +140,7 @@ describe('PolicyEditorApp component', () => {
it('adds a new rule', async () => {
expect(wrapper.findAll(PolicyRuleBuilder).length).toEqual(0);
const button = wrapper.find("[data-testid='add-rule']");
const button = findAddRuleButton();
button.vm.$emit('click');
button.vm.$emit('click');
await wrapper.vm.$nextTick();
......@@ -115,4 +158,34 @@ describe('PolicyEditorApp component', () => {
expect(builder.props().endpointSelectorDisabled).toEqual(idx !== 0);
});
});
it('updates yaml editor value on switch to yaml editor', async () => {
wrapper.find("[id='policyName']").vm.$emit('input', 'test-policy');
wrapper.find("[data-testid='editor-mode']").vm.$emit('input', EditorModeYAML);
await wrapper.vm.$nextTick();
const editor = findNetworkPolicyEditor();
expect(editor.exists()).toBe(true);
expect(fromYaml(editor.props('value'))).toMatchObject({
name: 'test-policy',
});
});
describe('given there is a yaml parsing error', () => {
beforeEach(() => {
factory({
data: () => ({
yamlEditorError: {},
}),
});
});
it('renders parsing error alert', () => {
expect(findYAMLParsingAlert().exists()).toBe(true);
});
it('disables add rule button', () => {
expect(findAddRuleButton().props('disabled')).toBe(true);
});
});
});
......@@ -16266,6 +16266,9 @@ msgstr ""
msgid "NetworkPolicies|Rule mode"
msgstr ""
msgid "NetworkPolicies|Rule mode is unavailable for this policy. In some cases, we cannot parse the YAML file back into the rules editor."
msgstr ""
msgid "NetworkPolicies|Rules"
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