diff --git a/ee/app/assets/javascripts/threat_monitoring/components/network_policy_list.vue b/ee/app/assets/javascripts/threat_monitoring/components/network_policy_list.vue index 960df21168d4938a5c9b1885a4398b5a74c94440..57c66eebb6f434e05825945baf6bb3c8a50d3a53 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/network_policy_list.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/network_policy_list.vue @@ -15,6 +15,8 @@ import { getTimeago } from '~/lib/utils/datetime_utility'; import { setUrlFragment } from '~/lib/utils/url_utility'; import EnvironmentPicker from './environment_picker.vue'; import NetworkPolicyEditor from './network_policy_editor.vue'; +import PolicyDrawer from './policy_editor/policy_drawer.vue'; +import { CiliumNetworkPolicyKind } from './policy_editor/constants'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -30,6 +32,7 @@ export default { GlToggle, EnvironmentPicker, NetworkPolicyEditor, + PolicyDrawer, }, mixins: [glFeatureFlagsMixin()], props: { @@ -71,6 +74,14 @@ export default { hasAutoDevopsPolicy() { return this.policiesWithDefaults.some(policy => policy.isAutodevops); }, + shouldShowCiliumDrawer() { + if (!this.hasSelectedPolicy) return false; + + return ( + this.glFeatures.networkPolicyEditor && + this.selectedPolicy.manifest.includes(CiliumNetworkPolicyKind) + ); + }, }, methods: { ...mapActions('networkPolicies', ['createPolicy', 'updatePolicy']), @@ -233,17 +244,23 @@ export default { </template> <template> <div v-if="hasSelectedPolicy"> - <h5>{{ s__('NetworkPolicies|Policy definition') }}</h5> - <p>{{ s__("NetworkPolicies|Define this policy's location, conditions and actions.") }}</p> - <div class="gl-p-3 gl-bg-gray-50"> - <network-policy-editor - ref="policyEditor" - v-model="selectedPolicy.manifest" - class="network-policy-editor" - /> + <policy-drawer v-if="shouldShowCiliumDrawer" v-model="selectedPolicy.manifest" /> + + <div v-else> + <h5>{{ s__('NetworkPolicies|Policy definition') }}</h5> + <p> + {{ s__("NetworkPolicies|Define this policy's location, conditions and actions.") }} + </p> + <div class="gl-p-3 gl-bg-gray-50"> + <network-policy-editor + ref="policyEditor" + v-model="selectedPolicy.manifest" + class="network-policy-editor" + /> + </div> </div> - <h5 class="mt-4">{{ s__('NetworkPolicies|Enforcement status') }}</h5> + <h5 class="gl-mt-6">{{ s__('NetworkPolicies|Enforcement status') }}</h5> <p>{{ s__('NetworkPolicies|Choose whether to enforce this policy.') }}</p> <gl-toggle v-model="selectedPolicy.isEnabled" data-testid="policyToggle" /> </div> diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js index edef15c8db9308aa79efcabc1be74c804e42bcf6..daf49781038630b2c015640dbb092810fd74dc5c 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js @@ -29,3 +29,5 @@ export const PortMatchModeAny = 'any'; export const PortMatchModePortProtocol = 'port/protocol'; export const DisabledByLabel = 'network-policy.gitlab.com/disabled_by'; + +export const CiliumNetworkPolicyKind = 'CiliumNetworkPolicy'; diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/from_yaml.js b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/from_yaml.js index 8b413af62ec9806b7cce9d908f5e99a1624c05cb..5f7afdf28676adc6ff70d2b4384c27ae77d30ba0 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/from_yaml.js +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/from_yaml.js @@ -112,7 +112,7 @@ function parseRule(item, direction) { https://docs.cilium.io/en/v1.8/policy/language */ export default function fromYaml(manifest) { - const { metadata, spec } = safeLoad(manifest, { json: true }); + const { description, metadata, spec } = safeLoad(manifest, { json: true }); const { endpointSelector = {}, ingress = [], egress = [] } = spec; const matchLabels = endpointSelector.matchLabels || {}; @@ -131,7 +131,7 @@ export default function fromYaml(manifest) { return { name: metadata.name, - description: spec.description, + description, isEnabled: !Object.keys(matchLabels).includes(DisabledByLabel), endpointMatchMode: endpointLabels.length > 0 ? EndpointMatchModeLabel : EndpointMatchModeAny, endpointLabels: endpointLabels.join(' '), diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/to_yaml.js b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/to_yaml.js index 658c93b4a8f4961a3ae02262aefa5a23ea171d34..56aaa37a42f3d6a7bcdfc327d3506eb2f6c0af55 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/to_yaml.js +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/lib/to_yaml.js @@ -1,20 +1,16 @@ import { safeDump } from 'js-yaml'; import { ruleSpec } from './rules'; import { labelSelector } from './utils'; -import { EndpointMatchModeAny, DisabledByLabel } from '../constants'; +import { EndpointMatchModeAny, DisabledByLabel, CiliumNetworkPolicyKind } from '../constants'; /* Return kubernetes resource specification object for a policy. */ -function spec({ description, rules, isEnabled, endpointMatchMode, endpointLabels }) { +function spec({ rules, isEnabled, endpointMatchMode, endpointLabels }) { const matchLabels = endpointMatchMode === EndpointMatchModeAny ? {} : labelSelector(endpointLabels); const policySpec = {}; - if (description?.length > 0) { - policySpec.description = description; - } - policySpec.endpointSelector = Object.keys(matchLabels).length > 0 ? { matchLabels } : {}; rules.forEach(rule => { const { direction } = rule; @@ -37,14 +33,22 @@ function spec({ description, rules, isEnabled, endpointMatchMode, endpointLabels Return yaml representation of a policy. */ export default function toYaml(policy) { - const { name } = policy; + const { name, description } = policy; const policySpec = { apiVersion: 'cilium.io/v2', - kind: 'CiliumNetworkPolicy', + kind: CiliumNetworkPolicyKind, + }; + + if (description?.length > 0) { + policySpec.description = description; + } + + // We want description at a specific position to have yaml in a common form. + Object.assign(policySpec, { metadata: { name }, spec: spec(policy), - }; + }); return safeDump(policySpec, { noArrayIndent: true }); } diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_drawer.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_drawer.vue new file mode 100644 index 0000000000000000000000000000000000000000..9e9f4093979ffc006bba5070e8510d74dd9bc0fa --- /dev/null +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_drawer.vue @@ -0,0 +1,56 @@ +<script> +import { GlFormTextarea } from '@gitlab/ui'; +import PolicyPreview from './policy_preview.vue'; +import fromYaml from './lib/from_yaml'; +import toYaml from './lib/to_yaml'; +import humanizeNetworkPolicy from './lib/humanize'; + +export default { + components: { + GlFormTextarea, + PolicyPreview, + }, + props: { + value: { + type: String, + required: true, + }, + }, + computed: { + policy() { + return fromYaml(this.value); + }, + humanizedPolicy() { + return humanizeNetworkPolicy(this.policy); + }, + policyYaml() { + return toYaml(this.policy); + }, + }, + methods: { + updateManifest(description) { + const manifest = toYaml({ ...this.policy, description }); + this.$emit('input', manifest); + }, + }, +}; +</script> + +<template> + <div> + <h4>{{ s__('NetworkPolicies|Policy description') }}</h4> + + <h5 class="gl-mt-6">{{ s__('NetworkPolicies|Policy type') }}</h5> + <p>{{ s__('NetworkPolicies|Network Policy') }}</p> + + <h5 class="gl-mt-6">{{ s__('NetworkPolicies|Description') }}</h5> + <gl-form-textarea :value="policy.description" @input="updateManifest" /> + + <policy-preview + class="gl-mt-4" + :initial-tab="1" + :policy-yaml="policyYaml" + :policy-description="humanizedPolicy" + /> + </div> +</template> diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_preview.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_preview.vue index a0846d22f520efff230a04ff555e8be4521a451e..29fb45f66133d456707b7241b6eb4c29d534e8cd 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_preview.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_preview.vue @@ -18,13 +18,21 @@ export default { type: String, required: true, }, + initialTab: { + type: Number, + required: false, + default: 0, + }, + }, + data() { + return { selectedTab: this.initialTab }; }, safeHtmlConfig: { ALLOWED_TAGS: ['strong', 'br'] }, }; </script> <template> - <gl-tabs content-class="gl-pt-0"> + <gl-tabs v-model="selectedTab" 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 diff --git a/ee/spec/frontend/threat_monitoring/components/__snapshots__/network_policy_list_spec.js.snap b/ee/spec/frontend/threat_monitoring/components/__snapshots__/network_policy_list_spec.js.snap index 4878c0456b33a45846eb0bfd493b89cac6599502..d5ab40407b4340c3cfe6441be7de9524be9e9eba 100644 --- a/ee/spec/frontend/threat_monitoring/components/__snapshots__/network_policy_list_spec.js.snap +++ b/ee/spec/frontend/threat_monitoring/components/__snapshots__/network_policy_list_spec.js.snap @@ -6,10 +6,10 @@ exports[`NetworkPolicyList component renders policies table 1`] = ` <table aria-busy="false" aria-colcount="3" - aria-describedby="__BVID__143__caption_" + aria-describedby="__BVID__334__caption_" aria-multiselectable="false" class="table b-table gl-table table-hover b-table-stacked-md b-table-selectable b-table-select-single" - id="__BVID__143" + id="__BVID__334" role="table" > <!----> diff --git a/ee/spec/frontend/threat_monitoring/components/network_policy_list_spec.js b/ee/spec/frontend/threat_monitoring/components/network_policy_list_spec.js index 21db4da622fd6b899aeb9cd95bd05cd2892488db..ecd56134dd5938d64321c86921d480ecfc79c556 100644 --- a/ee/spec/frontend/threat_monitoring/components/network_policy_list_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/network_policy_list_spec.js @@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'; import { GlTable } from '@gitlab/ui'; import createStore from 'ee/threat_monitoring/store'; import NetworkPolicyList from 'ee/threat_monitoring/components/network_policy_list.vue'; +import PolicyDrawer from 'ee/threat_monitoring/components/policy_editor/policy_drawer.vue'; import { PREDEFINED_NETWORK_POLICIES } from 'ee/threat_monitoring/constants'; import { useFakeDate } from 'helpers/fake_date'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; @@ -80,6 +81,43 @@ describe('NetworkPolicyList component', () => { const button = wrapper.find('[data-testid="new-policy"]'); expect(button.exists()).toBe(true); }); + + it('does not render the new policy drawer', () => { + expect(wrapper.find(PolicyDrawer).exists()).toBe(false); + }); + + describe('given selected policy is a cilium policy', () => { + beforeEach(() => { + factory({ + provide: { + glFeatures: { + networkPolicyEditor: true, + }, + }, + data: () => ({ + selectedPolicyName: 'policy', + }), + state: { + policies: [ + { + name: 'policy', + isEnabled: false, + manifest: `apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: test-policy +spec: + endpointSelector: {}`, + }, + ], + }, + }); + }); + + it('renders the new policy drawer', () => { + expect(wrapper.find(PolicyDrawer).exists()).toBe(true); + }); + }); }); it('renders policies table', () => { diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_drawer_spec.js.snap b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_drawer_spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..73e925fa43f2f82ad1b7a108bffc834bad872354 --- /dev/null +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_drawer_spec.js.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PolicyDrawer component renders policy preview tabs 1`] = ` +<div> + <h4> + Policy description + </h4> + + <h5 + class="gl-mt-6" + > + Policy type + </h5> + + <p> + Network Policy + </p> + + <h5 + class="gl-mt-6" + > + Description + </h5> + + <gl-form-textarea-stub + noresize="true" + value="test description" + /> + + <policy-preview-stub + class="gl-mt-4" + initialtab="1" + policydescription="Deny all traffic" + policyyaml="apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +description: test description +metadata: + name: test-policy +spec: + endpointSelector: + matchLabels: + network-policy.gitlab.com/disabled_by: gitlab +" + /> +</div> +`; diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap index 772ce2bc82560a23999e66b6e59f7a2d3ee1503f..29ede8160db38129bd35917d54856b31586ff1f0 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap @@ -186,6 +186,7 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = ` </h5> <policy-preview-stub + initialtab="0" policydescription="Deny all traffic" policyyaml="apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_preview_spec.js.snap b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_preview_spec.js.snap index c5e8b72b439287efa4df421ebdab847482506e7c..22ba81e824ddac5f4fb43afe56987e54b13f9616 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_preview_spec.js.snap +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_preview_spec.js.snap @@ -4,6 +4,7 @@ exports[`PolicyPreview component renders policy preview tabs 1`] = ` <gl-tabs-stub contentclass="gl-pt-0" theme="indigo" + value="0" > <gl-tab-stub title=".yaml" diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/lib/to_yaml_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/lib/to_yaml_spec.js index faf051e046cb9be5367bd43b88d977ccbd5204ea..8f976270165bc170a30ce095d93823f169cac714 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/lib/to_yaml_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/lib/to_yaml_spec.js @@ -29,10 +29,10 @@ spec: it('returns yaml representation', () => { expect(toYaml(policy)).toEqual(`apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy +description: test description metadata: name: test-policy spec: - description: test description endpointSelector: matchLabels: network-policy.gitlab.com/disabled_by: gitlab diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_drawer_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_drawer_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..dbd2d3f59d4cb8bb9b88cdb221440903f81b2ada --- /dev/null +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_drawer_spec.js @@ -0,0 +1,47 @@ +import { GlFormTextarea } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import PolicyDrawer from 'ee/threat_monitoring/components/policy_editor/policy_drawer.vue'; +import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml'; +import fromYaml from 'ee/threat_monitoring/components/policy_editor/lib/from_yaml'; + +describe('PolicyDrawer component', () => { + let wrapper; + const policy = { + name: 'test-policy', + description: 'test description', + endpointLabels: '', + rules: [], + }; + + const factory = ({ propsData } = {}) => { + wrapper = shallowMount(PolicyDrawer, { + propsData: { + ...propsData, + }, + }); + }; + + beforeEach(() => { + factory({ + propsData: { + value: toYaml(policy), + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders policy preview tabs', () => { + expect(wrapper.find('div').element).toMatchSnapshot(); + }); + + it('emits input event on description change', () => { + wrapper.find(GlFormTextarea).vm.$emit('input', 'new description'); + + expect(wrapper.emitted().input.length).toEqual(1); + const updatedPolicy = fromYaml(wrapper.emitted().input[0][0]); + expect(updatedPolicy.description).toEqual('new description'); + }); +}); diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js index 5ca8dfa2ab7768764913a9518cbf3fb2e29dae5a..982a5d1250ea245c3dddcc3985da8ab9850a8fa4 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js @@ -82,10 +82,10 @@ describe('PolicyEditorApp component', () => { it('updates policy on yaml editor value change', async () => { const manifest = `apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy +description: test description metadata: name: test-policy spec: - description: test description endpointSelector: matchLabels: network-policy.gitlab.com/disabled_by: gitlab diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_preview_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_preview_spec.js index e2fa4c1fcc276a439e2cd4161946b263b04a0d50..5c2c8b9be9180d8520d28b7afaeecd7b41e67d9b 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_preview_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_preview_spec.js @@ -29,4 +29,20 @@ describe('PolicyPreview component', () => { it('renders policy preview tabs', () => { expect(wrapper.find(GlTabs).element).toMatchSnapshot(); }); + + describe('with initialTab', () => { + beforeEach(() => { + factory({ + propsData: { + policyYaml: 'foo', + policyDescription: 'bar', + initialTab: 1, + }, + }); + }); + + it('selects initial tab', () => { + expect(wrapper.find(GlTabs).attributes().value).toEqual('1'); + }); + }); });