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 { ...@@ -9,8 +9,10 @@ import {
GlSegmentedControl, GlSegmentedControl,
GlButton, GlButton,
GlAlert, GlAlert,
GlModal,
GlModalDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import EnvironmentPicker from '../environment_picker.vue'; import EnvironmentPicker from '../environment_picker.vue';
import NetworkPolicyEditor from '../network_policy_editor.vue'; import NetworkPolicyEditor from '../network_policy_editor.vue';
...@@ -38,12 +40,14 @@ export default { ...@@ -38,12 +40,14 @@ export default {
GlSegmentedControl, GlSegmentedControl,
GlButton, GlButton,
GlAlert, GlAlert,
GlModal,
EnvironmentPicker, EnvironmentPicker,
NetworkPolicyEditor, NetworkPolicyEditor,
PolicyRuleBuilder, PolicyRuleBuilder,
PolicyPreview, PolicyPreview,
PolicyActionPicker, PolicyActionPicker,
}, },
directives: { GlModal: GlModalDirective },
props: { props: {
threatMonitoringPath: { threatMonitoringPath: {
type: String, type: String,
...@@ -82,7 +86,12 @@ export default { ...@@ -82,7 +86,12 @@ export default {
return toYaml(this.policy); return toYaml(this.policy);
}, },
...mapState('threatMonitoring', ['currentEnvironmentId']), ...mapState('threatMonitoring', ['currentEnvironmentId']),
...mapState('networkPolicies', ['errorUpdatingPolicy']), ...mapState('networkPolicies', [
'isUpdatingPolicy',
'isRemovingPolicy',
'errorUpdatingPolicy',
'errorRemovingPolicy',
]),
shouldShowRuleEditor() { shouldShowRuleEditor() {
return this.editorMode === EditorModeRule; return this.editorMode === EditorModeRule;
}, },
...@@ -100,13 +109,16 @@ export default { ...@@ -100,13 +109,16 @@ export default {
? s__('NetworkPolicies|Save changes') ? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy'); : s__('NetworkPolicies|Create policy');
}, },
deleteModalTitle() {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policy.name });
},
}, },
created() { created() {
this.fetchEnvironments(); this.fetchEnvironments();
}, },
methods: { methods: {
...mapActions('threatMonitoring', ['fetchEnvironments']), ...mapActions('threatMonitoring', ['fetchEnvironments']),
...mapActions('networkPolicies', ['createPolicy', 'updatePolicy']), ...mapActions('networkPolicies', ['createPolicy', 'updatePolicy', 'deletePolicy']),
addRule() { addRule() {
this.policy.rules.push(buildRule(RuleTypeEndpoint)); this.policy.rules.push(buildRule(RuleTypeEndpoint));
}, },
...@@ -120,6 +132,9 @@ export default { ...@@ -120,6 +132,9 @@ export default {
const rule = this.policy.rules[ruleIdx]; const rule = this.policy.rules[ruleIdx];
this.policy.rules.splice(ruleIdx, 1, buildRule(ruleType, rule)); this.policy.rules.splice(ruleIdx, 1, buildRule(ruleType, rule));
}, },
removeRule(ruleIdx) {
this.policy.rules.splice(ruleIdx, 1);
},
loadYaml(manifest) { loadYaml(manifest) {
this.yamlEditorValue = manifest; this.yamlEditorValue = manifest;
this.yamlEditorError = null; this.yamlEditorError = null;
...@@ -148,6 +163,13 @@ export default { ...@@ -148,6 +163,13 @@ export default {
if (!this.errorUpdatingPolicy) redirectTo(this.threatMonitoringPath); 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') }], policyTypes: [{ value: 'networkPolicy', text: s__('NetworkPolicies|Network Policy') }],
editorModes: [ editorModes: [
...@@ -157,6 +179,16 @@ export default { ...@@ -157,6 +179,16 @@ export default {
parsingErrorMessage: s__( parsingErrorMessage: s__(
'NetworkPolicies|Rule mode is unavailable for this policy. In some cases, we cannot parse the YAML file back into the rules editor.', '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> </script>
...@@ -233,6 +265,7 @@ export default { ...@@ -233,6 +265,7 @@ export default {
@rule-type-change="updateRuleType(idx, $event)" @rule-type-change="updateRuleType(idx, $event)"
@endpoint-match-mode-change="updateEndpointMatchMode" @endpoint-match-mode-change="updateEndpointMatchMode"
@endpoint-labels-change="updateEndpointLabels" @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"> <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 { ...@@ -282,13 +315,36 @@ export default {
category="primary" category="primary"
variant="success" variant="success"
data-testid="save-policy" data-testid="save-policy"
:loading="isUpdatingPolicy"
@click="savePolicy" @click="savePolicy"
>{{ saveButtonText }}</gl-button >{{ 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">{{ <gl-button category="secondary" variant="default" :href="threatMonitoringPath">{{
__('Cancel') __('Cancel')
}}</gl-button> }}</gl-button>
</div> </div>
</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> </section>
</template> </template>
<script> <script>
import { GlSprintf, GlForm, GlFormSelect, GlFormInput } from '@gitlab/ui'; import { GlSprintf, GlForm, GlFormSelect, GlFormInput, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { import {
RuleTypeNetwork, RuleTypeNetwork,
...@@ -25,6 +25,7 @@ export default { ...@@ -25,6 +25,7 @@ export default {
GlForm, GlForm,
GlFormSelect, GlFormSelect,
GlFormInput, GlFormInput,
GlButton,
PolicyRuleEndpoint, PolicyRuleEndpoint,
PolicyRuleEntity, PolicyRuleEntity,
'policy-rule-cidr': PolicyRuleCIDR, 'policy-rule-cidr': PolicyRuleCIDR,
...@@ -131,7 +132,7 @@ export default { ...@@ -131,7 +132,7 @@ export default {
<template> <template>
<div <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-form inline @submit.prevent>
<gl-sprintf :message="sprintfTemplate"> <gl-sprintf :message="sprintfTemplate">
...@@ -226,5 +227,13 @@ export default { ...@@ -226,5 +227,13 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</gl-form> </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> </div>
</template> </template>
...@@ -88,3 +88,27 @@ export const updatePolicy = ({ state, commit }, { environmentId, policy }) => { ...@@ -88,3 +88,27 @@ export const updatePolicy = ({ state, commit }, { environmentId, policy }) => {
commitPolicyError(commit, types.RECEIVE_UPDATE_POLICY_ERROR, error?.response?.data), 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'; ...@@ -11,3 +11,7 @@ export const RECEIVE_CREATE_POLICY_ERROR = 'RECEIVE_CREATE_POLICY_ERROR';
export const REQUEST_UPDATE_POLICY = 'REQUEST_UPDATE_POLICY'; export const REQUEST_UPDATE_POLICY = 'REQUEST_UPDATE_POLICY';
export const RECEIVE_UPDATE_POLICY_SUCCESS = 'RECEIVE_UPDATE_POLICY_SUCCESS'; export const RECEIVE_UPDATE_POLICY_SUCCESS = 'RECEIVE_UPDATE_POLICY_SUCCESS';
export const RECEIVE_UPDATE_POLICY_ERROR = 'RECEIVE_UPDATE_POLICY_ERROR'; 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 { ...@@ -46,4 +46,17 @@ export default {
state.isUpdatingPolicy = false; state.isUpdatingPolicy = false;
state.errorUpdatingPolicy = true; 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 () => ({ ...@@ -5,4 +5,6 @@ export default () => ({
errorLoadingPolicies: false, errorLoadingPolicies: false,
isUpdatingPolicy: false, isUpdatingPolicy: false,
errorUpdatingPolicy: false, errorUpdatingPolicy: false,
isRemovingPolicy: false,
errorRemovingPolicy: false,
}); });
...@@ -226,6 +226,8 @@ spec: ...@@ -226,6 +226,8 @@ spec:
Create policy Create policy
</gl-button-stub> </gl-button-stub>
<!---->
<gl-button-stub <gl-button-stub
category="secondary" category="secondary"
href="/threat-monitoring" href="/threat-monitoring"
...@@ -237,5 +239,19 @@ spec: ...@@ -237,5 +239,19 @@ spec:
</gl-button-stub> </gl-button-stub>
</div> </div>
</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> </section>
`; `;
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
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 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';
...@@ -51,6 +52,7 @@ describe('PolicyEditorApp component', () => { ...@@ -51,6 +52,7 @@ describe('PolicyEditorApp component', () => {
const findNetworkPolicyEditor = () => wrapper.find(NetworkPolicyEditor); const findNetworkPolicyEditor = () => wrapper.find(NetworkPolicyEditor);
const findPolicyName = () => wrapper.find("[id='policyName']"); const findPolicyName = () => wrapper.find("[id='policyName']");
const findSavePolicy = () => wrapper.find("[data-testid='save-policy']"); const findSavePolicy = () => wrapper.find("[data-testid='save-policy']");
const findDeletePolicy = () => wrapper.find("[data-testid='delete-policy']");
beforeEach(() => { beforeEach(() => {
factory(); factory();
...@@ -72,6 +74,10 @@ describe('PolicyEditorApp component', () => { ...@@ -72,6 +74,10 @@ describe('PolicyEditorApp component', () => {
expect(findYAMLParsingAlert().exists()).toBe(false); expect(findYAMLParsingAlert().exists()).toBe(false);
}); });
it('does not render delete button', () => {
expect(findDeletePolicy().exists()).toBe(false);
});
describe('given .yaml editor mode is enabled', () => { describe('given .yaml editor mode is enabled', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory({
...@@ -171,6 +177,16 @@ spec: ...@@ -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 () => { it('updates yaml editor value on switch to yaml editor', async () => {
findPolicyName().vm.$emit('input', 'test-policy'); findPolicyName().vm.$emit('input', 'test-policy');
wrapper.find("[data-testid='editor-mode']").vm.$emit('input', EditorModeYAML); wrapper.find("[data-testid='editor-mode']").vm.$emit('input', EditorModeYAML);
...@@ -281,5 +297,27 @@ spec: ...@@ -281,5 +297,27 @@ spec:
expect(redirectTo).not.toHaveBeenCalledWith('/threat-monitoring'); 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', () => { ...@@ -103,6 +103,11 @@ describe('PolicyRuleBuilder component', () => {
expect(event[0]).toEqual([RuleTypeEntity]); 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', () => { it('renders only endpoint rule component', () => {
expect(findRuleEndpoint().exists()).toBe(true); expect(findRuleEndpoint().exists()).toBe(true);
expect(findRuleEntity().exists()).toBe(false); expect(findRuleEntity().exists()).toBe(false);
......
...@@ -340,4 +340,106 @@ describe('Network Policy actions', () => { ...@@ -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', () => { ...@@ -138,4 +138,46 @@ describe('Network Policies mutations', () => {
expect(state.errorUpdatingPolicy).toBe(true); 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 "" ...@@ -16376,6 +16376,9 @@ msgstr ""
msgid "NetworkPolicies|Allow all outbound traffic from %{selector} to %{ruleSelector} on %{ports}" msgid "NetworkPolicies|Allow all outbound traffic from %{selector} to %{ruleSelector} on %{ports}"
msgstr "" 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." msgid "NetworkPolicies|Choose whether to enforce this policy."
msgstr "" msgstr ""
...@@ -16385,6 +16388,12 @@ msgstr "" ...@@ -16385,6 +16388,12 @@ 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|Delete policy"
msgstr ""
msgid "NetworkPolicies|Delete policy: %{policy}"
msgstr ""
msgid "NetworkPolicies|Deny all traffic" msgid "NetworkPolicies|Deny all traffic"
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