Commit 81908c03 authored by Kushal Pandya's avatar Kushal Pandya

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

Implement policy removal flow

See merge request gitlab-org/gitlab!41713
parents 271228b7 a996f040
......@@ -9,8 +9,10 @@ import {
GlSegmentedControl,
GlButton,
GlAlert,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { s__, __, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import EnvironmentPicker from '../environment_picker.vue';
import NetworkPolicyEditor from '../network_policy_editor.vue';
......@@ -38,12 +40,14 @@ export default {
GlSegmentedControl,
GlButton,
GlAlert,
GlModal,
EnvironmentPicker,
NetworkPolicyEditor,
PolicyRuleBuilder,
PolicyPreview,
PolicyActionPicker,
},
directives: { GlModal: GlModalDirective },
props: {
threatMonitoringPath: {
type: String,
......@@ -82,7 +86,12 @@ export default {
return toYaml(this.policy);
},
...mapState('threatMonitoring', ['currentEnvironmentId']),
...mapState('networkPolicies', ['errorUpdatingPolicy']),
...mapState('networkPolicies', [
'isUpdatingPolicy',
'isRemovingPolicy',
'errorUpdatingPolicy',
'errorRemovingPolicy',
]),
shouldShowRuleEditor() {
return this.editorMode === EditorModeRule;
},
......@@ -100,13 +109,16 @@ export default {
? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy');
},
deleteModalTitle() {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policy.name });
},
},
created() {
this.fetchEnvironments();
},
methods: {
...mapActions('threatMonitoring', ['fetchEnvironments']),
...mapActions('networkPolicies', ['createPolicy', 'updatePolicy']),
...mapActions('networkPolicies', ['createPolicy', 'updatePolicy', 'deletePolicy']),
addRule() {
this.policy.rules.push(buildRule(RuleTypeEndpoint));
},
......@@ -120,6 +132,9 @@ export default {
const rule = this.policy.rules[ruleIdx];
this.policy.rules.splice(ruleIdx, 1, buildRule(ruleType, rule));
},
removeRule(ruleIdx) {
this.policy.rules.splice(ruleIdx, 1);
},
loadYaml(manifest) {
this.yamlEditorValue = manifest;
this.yamlEditorError = null;
......@@ -148,6 +163,13 @@ export default {
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: [
......@@ -157,6 +179,16 @@ export default {
parsingErrorMessage: s__(
'NetworkPolicies|Rule mode is unavailable for this policy. In some cases, we cannot parse the YAML file back into the rules editor.',
),
deleteModal: {
id: 'delete-modal',
secondary: {
text: s__('NetworkPolicies|Delete policy'),
attributes: { variant: 'danger' },
},
cancel: {
text: __('Cancel'),
},
},
};
</script>
......@@ -233,6 +265,7 @@ export default {
@rule-type-change="updateRuleType(idx, $event)"
@endpoint-match-mode-change="updateEndpointMatchMode"
@endpoint-labels-change="updateEndpointLabels"
@remove="removeRule(idx)"
/>
<div class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5">
......@@ -282,13 +315,36 @@ export default {
category="primary"
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" variant="default" :href="threatMonitoringPath">{{
__('Cancel')
}}</gl-button>
</div>
</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>
<script>
import { GlSprintf, GlForm, GlFormSelect, GlFormInput } from '@gitlab/ui';
import { GlSprintf, GlForm, GlFormSelect, GlFormInput, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import {
RuleTypeNetwork,
......@@ -25,6 +25,7 @@ export default {
GlForm,
GlFormSelect,
GlFormInput,
GlButton,
PolicyRuleEndpoint,
PolicyRuleEntity,
'policy-rule-cidr': PolicyRuleCIDR,
......@@ -131,7 +132,7 @@ export default {
<template>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base px-3 pt-3"
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base px-3 pt-3 gl-relative"
>
<gl-form inline @submit.prevent>
<gl-sprintf :message="sprintfTemplate">
......@@ -226,5 +227,13 @@ export default {
</template>
</gl-sprintf>
</gl-form>
<gl-button
icon="remove"
size="small"
class="gl-absolute gl-top-3 gl-right-3"
data-testid="remove-rule"
@click="$emit('remove')"
/>
</div>
</template>
......@@ -88,3 +88,27 @@ export const updatePolicy = ({ state, commit }, { environmentId, policy }) => {
commitPolicyError(commit, types.RECEIVE_UPDATE_POLICY_ERROR, error?.response?.data),
);
};
export const deletePolicy = ({ state, commit }, { environmentId, policy }) => {
if (!state.policiesEndpoint || !environmentId || !policy) {
return commitPolicyError(commit, types.RECEIVE_DELETE_POLICY_ERROR);
}
commit(types.REQUEST_DELETE_POLICY);
return axios
.delete(joinPaths(state.policiesEndpoint, policy.name), {
params: {
environment_id: environmentId,
manifest: policy.manifest,
},
})
.then(() => {
commit(types.RECEIVE_DELETE_POLICY_SUCCESS, {
policy,
});
})
.catch(error =>
commitPolicyError(commit, types.RECEIVE_DELETE_POLICY_ERROR, error?.response?.data),
);
};
......@@ -11,3 +11,7 @@ export const RECEIVE_CREATE_POLICY_ERROR = 'RECEIVE_CREATE_POLICY_ERROR';
export const REQUEST_UPDATE_POLICY = 'REQUEST_UPDATE_POLICY';
export const RECEIVE_UPDATE_POLICY_SUCCESS = 'RECEIVE_UPDATE_POLICY_SUCCESS';
export const RECEIVE_UPDATE_POLICY_ERROR = 'RECEIVE_UPDATE_POLICY_ERROR';
export const REQUEST_DELETE_POLICY = 'REQUEST_DELETE_POLICY';
export const RECEIVE_DELETE_POLICY_SUCCESS = 'RECEIVE_DELETE_POLICY_SUCCESS';
export const RECEIVE_DELETE_POLICY_ERROR = 'RECEIVE_DELETE_POLICY_ERROR';
......@@ -46,4 +46,17 @@ export default {
state.isUpdatingPolicy = false;
state.errorUpdatingPolicy = true;
},
[types.REQUEST_DELETE_POLICY](state) {
state.isRemovingPolicy = true;
state.errorRemovingPolicy = false;
},
[types.RECEIVE_DELETE_POLICY_SUCCESS](state, { policy }) {
state.policies = state.policies.filter(({ name }) => name !== policy.name);
state.isRemovingPolicy = false;
state.errorRemovingPolicy = false;
},
[types.RECEIVE_DELETE_POLICY_ERROR](state) {
state.isRemovingPolicy = false;
state.errorRemovingPolicy = true;
},
};
......@@ -5,4 +5,6 @@ export default () => ({
errorLoadingPolicies: false,
isUpdatingPolicy: false,
errorUpdatingPolicy: false,
isRemovingPolicy: false,
errorRemovingPolicy: false,
});
......@@ -226,6 +226,8 @@ spec:
Create policy
</gl-button-stub>
<!---->
<gl-button-stub
category="secondary"
href="/threat-monitoring"
......@@ -237,5 +239,19 @@ spec:
</gl-button-stub>
</div>
</div>
<gl-modal-stub
actioncancel="[object Object]"
actionsecondary="[object Object]"
modalclass=""
modalid="delete-modal"
size="md"
title="Delete policy: "
titletag="h4"
>
Are you sure you want to delete this policy? This action cannot be undone.
</gl-modal-stub>
</section>
`;
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
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';
......@@ -51,6 +52,7 @@ describe('PolicyEditorApp component', () => {
const findNetworkPolicyEditor = () => wrapper.find(NetworkPolicyEditor);
const findPolicyName = () => wrapper.find("[id='policyName']");
const findSavePolicy = () => wrapper.find("[data-testid='save-policy']");
const findDeletePolicy = () => wrapper.find("[data-testid='delete-policy']");
beforeEach(() => {
factory();
......@@ -72,6 +74,10 @@ describe('PolicyEditorApp component', () => {
expect(findYAMLParsingAlert().exists()).toBe(false);
});
it('does not render delete button', () => {
expect(findDeletePolicy().exists()).toBe(false);
});
describe('given .yaml editor mode is enabled', () => {
beforeEach(() => {
factory({
......@@ -171,6 +177,16 @@ spec:
});
});
it('removes a new rule', async () => {
findAddRuleButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.findAll(PolicyRuleBuilder).length).toEqual(1);
wrapper.find(PolicyRuleBuilder).vm.$emit('remove');
await wrapper.vm.$nextTick();
expect(wrapper.findAll(PolicyRuleBuilder).length).toEqual(0);
});
it('updates yaml editor value on switch to yaml editor', async () => {
findPolicyName().vm.$emit('input', 'test-policy');
wrapper.find("[data-testid='editor-mode']").vm.$emit('input', EditorModeYAML);
......@@ -281,5 +297,27 @@ spec:
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.find(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');
});
});
});
......@@ -103,6 +103,11 @@ describe('PolicyRuleBuilder component', () => {
expect(event[0]).toEqual([RuleTypeEntity]);
});
it('emits remove upon remove-button click', () => {
wrapper.find("[data-testid='remove-rule']").trigger('click');
expect(wrapper.emitted().remove.length).toEqual(1);
});
it('renders only endpoint rule component', () => {
expect(findRuleEndpoint().exists()).toBe(true);
expect(findRuleEntity().exists()).toBe(false);
......
......@@ -340,4 +340,106 @@ describe('Network Policy actions', () => {
}));
});
});
describe('deletePolicy', () => {
describe('on success', () => {
beforeEach(() => {
mock
.onDelete(joinPaths(networkPoliciesEndpoint, policy.name), {
params: {
environment_id: environmentId,
manifest: policy.manifest,
},
})
.replyOnce(httpStatus.OK);
});
it('should dispatch the request and success actions', () =>
testAction(
actions.deletePolicy,
{ environmentId, policy },
state,
[
{ type: types.REQUEST_DELETE_POLICY },
{
type: types.RECEIVE_DELETE_POLICY_SUCCESS,
payload: { policy },
},
],
[],
));
});
describe('on error', () => {
const error = { error: 'foo' };
beforeEach(() => {
mock
.onDelete(joinPaths(networkPoliciesEndpoint, policy.name), {
params: {
environment_id: environmentId,
manifest: policy.manifest,
},
})
.replyOnce(500, error);
});
it('should dispatch the request and error actions', () =>
testAction(
actions.deletePolicy,
{ environmentId, policy },
state,
[
{ type: types.REQUEST_DELETE_POLICY },
{ type: types.RECEIVE_DELETE_POLICY_ERROR, payload: 'foo' },
],
[],
).then(() => {
expect(createFlash).toHaveBeenCalled();
}));
});
describe('with an empty endpoint', () => {
beforeEach(() => {
state.policiesEndpoint = '';
});
it('should dispatch RECEIVE_DELETE_POLICY_ERROR', () =>
testAction(
actions.deletePolicy,
{ environmentId, policy },
state,
[
{
type: types.RECEIVE_DELETE_POLICY_ERROR,
payload: s__('NetworkPolicies|Something went wrong, failed to update policy'),
},
],
[],
).then(() => {
expect(createFlash).toHaveBeenCalled();
}));
});
describe('without environment id', () => {
it('should dispatch RECEIVE_DELETE_POLICY_ERROR', () =>
testAction(
actions.deletePolicy,
{
environmentId: undefined,
policy,
},
state,
[
{
type: types.RECEIVE_DELETE_POLICY_ERROR,
payload: s__('NetworkPolicies|Something went wrong, failed to update policy'),
},
],
[],
).then(() => {
expect(createFlash).toHaveBeenCalled();
}));
});
});
});
......@@ -138,4 +138,46 @@ describe('Network Policies mutations', () => {
expect(state.errorUpdatingPolicy).toBe(true);
});
});
describe(types.REQUEST_DELETE_POLICY, () => {
beforeEach(() => {
mutations[types.REQUEST_DELETE_POLICY](state);
});
it('sets isRemovingPolicy to true and sets errorRemovingPolicy to false', () => {
expect(state.isRemovingPolicy).toBe(true);
expect(state.errorRemovingPolicy).toBe(false);
});
});
describe(types.RECEIVE_DELETE_POLICY_SUCCESS, () => {
const policy = { id: 1, name: 'production', manifest: 'foo' };
beforeEach(() => {
state.policies.push(policy);
mutations[types.RECEIVE_DELETE_POLICY_SUCCESS](state, {
policy,
});
});
it('removes the policy', () => {
expect(state.policies).not.toEqual(expect.objectContaining(policy));
});
it('sets isRemovingPolicy to false and sets errorRemovingPolicy to false', () => {
expect(state.isRemovingPolicy).toBe(false);
expect(state.errorRemovingPolicy).toBe(false);
});
});
describe(types.RECEIVE_DELETE_POLICY_ERROR, () => {
beforeEach(() => {
mutations[types.RECEIVE_DELETE_POLICY_ERROR](state);
});
it('sets isRemovingPolicy to false and sets errorRemovingPolicy to true', () => {
expect(state.isRemovingPolicy).toBe(false);
expect(state.errorRemovingPolicy).toBe(true);
});
});
});
......@@ -16376,6 +16376,9 @@ msgstr ""
msgid "NetworkPolicies|Allow all outbound traffic from %{selector} to %{ruleSelector} on %{ports}"
msgstr ""
msgid "NetworkPolicies|Are you sure you want to delete this policy? This action cannot be undone."
msgstr ""
msgid "NetworkPolicies|Choose whether to enforce this policy."
msgstr ""
......@@ -16385,6 +16388,12 @@ msgstr ""
msgid "NetworkPolicies|Define this policy's location, conditions and actions."
msgstr ""
msgid "NetworkPolicies|Delete policy"
msgstr ""
msgid "NetworkPolicies|Delete policy: %{policy}"
msgstr ""
msgid "NetworkPolicies|Deny all traffic"
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