Commit 0bf55b8d authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '297463-disable-alert-for-no-agent' into 'master'

Conditionally render policy alert editor

See merge request gitlab-org/gitlab!52036
parents 866a91f3 99ccc8a6
<script> <script>
import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import getAgentCount from '../../graphql/queries/get_agent_count.query.graphql';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export default { export default {
...@@ -7,6 +8,9 @@ export default { ...@@ -7,6 +8,9 @@ export default {
ACTION: s__( ACTION: s__(
'NetworkPolicies|%{labelStart}And%{labelEnd} %{spanStart}send an Alert to GitLab.%{spanEnd}', 'NetworkPolicies|%{labelStart}And%{labelEnd} %{spanStart}send an Alert to GitLab.%{spanEnd}',
), ),
AGENT_REQUIRED: s__(
'NetworkPolicies|Please %{installLinkStart}install%{installLinkEnd} and %{configureLinkStart}configure a Kubernetes Agent for this project%{configureLinkEnd} to enable alerts.',
),
BUTTON_LABEL: s__('NetworkPolicies|+ Add alert'), BUTTON_LABEL: s__('NetworkPolicies|+ Add alert'),
HIGH_VOLUME_WARNING: s__( HIGH_VOLUME_WARNING: s__(
`NetworkPolicies|Alerts are intended to be selectively used for a limited number of events that are potentially concerning and warrant a manual review. Alerts should not be used as a substitute for a SIEM or a logging tool. High volume alerts are likely to be dropped so as to preserve the stability of GitLab's integration with Kubernetes.`, `NetworkPolicies|Alerts are intended to be selectively used for a limited number of events that are potentially concerning and warrant a manual review. Alerts should not be used as a substitute for a SIEM or a logging tool. High volume alerts are likely to be dropped so as to preserve the stability of GitLab's integration with Kubernetes.`,
...@@ -15,12 +19,44 @@ export default { ...@@ -15,12 +19,44 @@ export default {
components: { components: {
GlAlert, GlAlert,
GlButton, GlButton,
GlLink,
GlSprintf, GlSprintf,
}, },
inject: {
configureAgentHelpPath: { type: String, default: '' },
createAgentHelpPath: { type: String, default: '' },
projectPath: { type: String, default: '' },
},
props: { props: {
policyAlert: { policyAlert: { type: Boolean, required: true },
type: Boolean, },
required: true, apollo: {
agentCount: {
query: getAgentCount,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
return data?.project?.clusterAgents?.count || 0;
},
},
},
data() {
return {
agentCount: 0,
};
},
computed: {
agentLoading() {
return this.$apollo.queries.agentCount.loading;
},
isAgentInstalled() {
return Boolean(this.agentCount) && !this.agentLoading;
},
spacingClass() {
return { 'gl-mt-5': !this.policyAlert && this.isAgentInstalled };
}, },
}, },
}; };
...@@ -28,18 +64,45 @@ export default { ...@@ -28,18 +64,45 @@ export default {
<template> <template>
<div> <div>
<gl-alert v-if="policyAlert" variant="warning" :dismissible="false" class="gl-mt-5"> <gl-alert
v-if="!isAgentInstalled"
variant="danger"
:dismissible="false"
class="gl-mt-5"
data-testid="policy-alert-no-agent"
>
<gl-sprintf :message="$options.i18n.AGENT_REQUIRED">
<template #installLink="{ content }">
<gl-link :href="createAgentHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
<template #configureLink="{ content }">
<gl-link :href="configureAgentHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert
v-else-if="policyAlert"
variant="warning"
:dismissible="false"
class="gl-mt-5"
data-testid="policy-alert-high-volume"
>
{{ $options.i18n.HIGH_VOLUME_WARNING }} {{ $options.i18n.HIGH_VOLUME_WARNING }}
</gl-alert> </gl-alert>
<div <div
class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-5" class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-5"
:class="{ 'gl-mt-5': !policyAlert }" :class="spacingClass"
> >
<gl-button <gl-button
v-if="!policyAlert" v-if="!policyAlert"
variant="link" variant="link"
category="primary" category="primary"
data-testid="add-alert" data-testid="add-alert"
:disabled="!isAgentInstalled"
@click="$emit('update-alert', !policyAlert)" @click="$emit('update-alert', !policyAlert)"
> >
{{ $options.i18n.BUTTON_LABEL }} {{ $options.i18n.BUTTON_LABEL }}
...@@ -49,11 +112,11 @@ export default { ...@@ -49,11 +112,11 @@ export default {
class="gl-w-full gl-display-flex gl-justify-content-space-between gl-align-items-center" class="gl-w-full gl-display-flex gl-justify-content-space-between gl-align-items-center"
> >
<span> <span>
<gl-sprintf :message="$options.i18n.ACTION"> <gl-sprintf :message="$options.i18n.ACTION" data-testid="policy-alert-message">
<template #label="{ content }"> <template #label="{ content }">
<label for="actionType" class="text-uppercase gl-font-lg gl-mr-4 gl-mb-0">{{ <label for="actionType" class="text-uppercase gl-font-lg gl-mr-4 gl-mb-0">
content {{ content }}
}}</label> </label>
</template> </template>
<template #span="{ content }"> <template #span="{ content }">
<span>{{ content }}</span> <span>{{ content }}</span>
......
query getAgentCount($projectPath: ID!) {
project(fullPath: $projectPath) {
clusterAgents {
count
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import PolicyEditorApp from './components/policy_editor/policy_editor.vue'; import PolicyEditorApp from './components/policy_editor/policy_editor.vue';
import createStore from './store'; import createStore from './store';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => { export default () => {
const el = document.querySelector('#js-policy-builder-app'); const el = document.querySelector('#js-policy-builder-app');
const { const {
environmentsEndpoint, environmentsEndpoint,
configureAgentHelpPath,
createAgentHelpPath,
networkPoliciesEndpoint, networkPoliciesEndpoint,
threatMonitoringPath, threatMonitoringPath,
policy, policy,
projectPath,
environmentId, environmentId,
} = el.dataset; } = el.dataset;
...@@ -31,6 +42,12 @@ export default () => { ...@@ -31,6 +42,12 @@ export default () => {
return new Vue({ return new Vue({
el, el,
apolloProvider,
provide: {
configureAgentHelpPath,
createAgentHelpPath,
projectPath,
},
store, store,
render(createElement) { render(createElement) {
return createElement(PolicyEditorApp, { props }); return createElement(PolicyEditorApp, { props });
......
# frozen_string_literal: true
module PolicyHelper
def policy_details(project, policy = nil, environment = nil)
return unless project
details = {
network_policies_endpoint: project_security_network_policies_path(project),
configure_agent_help_path: help_page_url('user/clusters/agent/repository.html'),
create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'),
environments_endpoint: project_environments_path(project),
project_path: project.full_path,
threat_monitoring_path: project_threat_monitoring_path(project)
}
return details unless policy && environment
edit_details = {
policy: policy.to_json,
environment_id: environment.id
}
details.merge(edit_details)
end
end
- add_to_breadcrumbs s_("ThreatMonitoring|Threat Monitoring"), project_threat_monitoring_path(@project) - add_to_breadcrumbs s_("ThreatMonitoring|Threat Monitoring"), project_threat_monitoring_path(@project)
- breadcrumb_title @policy_name - breadcrumb_title @policy_name
- page_title s_("NetworkPolicies|Policy editor") - page_title s_("NetworkPolicies|Policy editor")
- policy_details = policy_details(@project, @policy, @environment)
#js-policy-builder-app{ data: { network_policies_endpoint: project_security_network_policies_path(@project), #js-policy-builder-app{ data: policy_details }
environments_endpoint: project_environments_path(@project),
threat_monitoring_path: project_threat_monitoring_path(@project),
policy: @policy.to_json,
environment_id: @environment.id,
} }
- add_to_breadcrumbs s_("ThreatMonitoring|Threat Monitoring"), project_threat_monitoring_path(@project) - add_to_breadcrumbs s_("ThreatMonitoring|Threat Monitoring"), project_threat_monitoring_path(@project)
- breadcrumb_title s_("NetworkPolicies|New policy") - breadcrumb_title s_("NetworkPolicies|New policy")
- page_title s_("NetworkPolicies|Policy editor") - page_title s_("NetworkPolicies|Policy editor")
- policy_details = policy_details(@project)
#js-policy-builder-app{ data: { network_policies_endpoint: project_security_network_policies_path(@project), #js-policy-builder-app{ data: policy_details }
environments_endpoint: project_environments_path(@project),
threat_monitoring_path: project_threat_monitoring_path(@project),
} }
import { shallowMount } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlAlert, GlSprintf } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import VueApollo from 'vue-apollo';
import PolicyAlertPicker from 'ee/threat_monitoring/components/policy_editor/policy_alert_picker.vue'; import PolicyAlertPicker from 'ee/threat_monitoring/components/policy_editor/policy_alert_picker.vue';
import getAgentCount from 'ee/threat_monitoring/graphql/queries/get_agent_count.query.graphql';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('PolicyAlertPicker component', () => { describe('PolicyAlertPicker component', () => {
let wrapper; let wrapper;
const defaultProps = { policyAlert: false }; const createMockApolloProvider = ({ agentCount }) => {
const getAgentCountHandler = jest
.fn()
.mockResolvedValue({ data: { project: { clusterAgents: { count: agentCount } } } });
const findAddAlertButton = () => wrapper.find("[data-testid='add-alert']"); return createMockApollo([[getAgentCount, getAgentCountHandler]]);
const findGlAlert = () => wrapper.find(GlAlert); };
const findGlSprintf = () => wrapper.find(GlSprintf);
const findRemoveAlertButton = () => wrapper.find("[data-testid='remove-alert']");
const createWrapper = ({ propsData = defaultProps } = {}) => { const defaultProps = { policyAlert: false };
wrapper = shallowMount(PolicyAlertPicker, {
propsData: { const findAddAlertButton = () => wrapper.findByTestId('add-alert');
...propsData, const findAlertMessage = () => wrapper.findByTestId('policy-alert-message');
}, const findHighVolumeAlert = () => wrapper.findByTestId('policy-alert-high-volume');
}); const findNoAgentAlert = () => wrapper.findByTestId('policy-alert-no-agent');
const findRemoveAlertButton = () => wrapper.findByTestId('remove-alert');
const createWrapper = async ({ propsData = defaultProps, agentCount = 1 } = {}) => {
const apolloProvider = createMockApolloProvider({ agentCount });
wrapper = extendedWrapper(
shallowMount(PolicyAlertPicker, {
apolloProvider,
localVue,
propsData: {
...propsData,
},
provide: {
configureAgentHelpPath: '',
createAgentHelpPath: '',
projectPath: '',
},
}),
);
await wrapper.vm.$nextTick();
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('default state', () => { describe('loading', () => {
beforeEach(() => { describe('default state', () => {
createWrapper(); beforeEach(async () => {
}); createWrapper();
});
it('does render the add alert button', () => { it('does render the enabled add alert button ', () => {
expect(findAddAlertButton().exists()).toBe(true); expect(findAddAlertButton().exists()).toBe(true);
}); expect(findAddAlertButton().props('disabled')).toBe(false);
});
it('does not render the high volume warning', () => { it('does not render the "no agent" alert', () => {
expect(findGlAlert().exists()).toBe(false); expect(findNoAgentAlert().exists()).toBe(false);
});
}); });
it('does not render the alert message', () => { describe('alert enabled', () => {
expect(findGlSprintf().exists()).toBe(false); beforeEach(async () => {
}); createWrapper({ propsData: { policyAlert: true } });
});
it('does not render the remove alert button', () => { it('does render the "high volume" alert', () => {
expect(findRemoveAlertButton().exists()).toBe(false); expect(findHighVolumeAlert().exists()).toBe(true);
}); });
it('does emit an event to add the alert', () => { it('does not render the "no agent" alert', () => {
findAddAlertButton().vm.$emit('click'); expect(findNoAgentAlert().exists()).toBe(false);
expect(wrapper.emitted('update-alert')).toEqual([[true]]); });
}); });
}); });
describe('alert enabled', () => { describe('default state', () => {
beforeEach(() => { describe('agent installed', () => {
createWrapper({ propsData: { policyAlert: true } }); beforeEach(async () => {
await createWrapper();
});
it('does render the enabled add alert button ', () => {
expect(findAddAlertButton().exists()).toBe(true);
expect(findAddAlertButton().props('disabled')).toBe(false);
});
it('does not render the "high volume" alert', () => {
expect(findHighVolumeAlert().exists()).toBe(false);
});
it('does not render the alert message', () => {
expect(findAlertMessage().exists()).toBe(false);
});
it('does not render the remove alert button', () => {
expect(findRemoveAlertButton().exists()).toBe(false);
});
it('does not render the "no agent" alert when there is an agent, ', () => {
expect(findNoAgentAlert().exists()).toBe(false);
});
it('does emit an event to add the alert', () => {
findAddAlertButton().vm.$emit('click');
expect(wrapper.emitted('update-alert')).toEqual([[true]]);
});
}); });
it('does not render the add alert button', () => { describe('no agent installed', () => {
expect(findAddAlertButton().exists()).toBe(false); beforeEach(async () => {
}); await createWrapper({ agentCount: 0 });
});
it('does render the high volume warning', () => { it('does render the "no agent" alert', () => {
expect(findGlAlert().exists()).toBe(true); expect(findNoAgentAlert().exists()).toBe(true);
}); });
it('does render the alert message', () => { it('does render the disabled add alert button ', async () => {
expect(findGlSprintf().exists()).toBe(true); expect(findAddAlertButton().exists()).toBe(true);
expect(findAddAlertButton().props('disabled')).toBe(true);
});
}); });
});
it('does render the remove alert button', () => { describe('alert enabled', () => {
expect(findRemoveAlertButton().exists()).toBe(true); describe('agent installed', () => {
beforeEach(async () => {
await createWrapper({ propsData: { policyAlert: true } });
});
it('does not render the add alert button', () => {
expect(findAddAlertButton().exists()).toBe(false);
});
it('does render the "high volume" alert', () => {
expect(findHighVolumeAlert().exists()).toBe(true);
});
it('does render the alert message', () => {
expect(findAlertMessage().exists()).toBe(true);
});
it('does render the remove alert button', () => {
expect(findRemoveAlertButton().exists()).toBe(true);
});
it('does not render the "no agent" alert', () => {
expect(findNoAgentAlert().exists()).toBe(false);
});
it('does emit an event to remove the alert', () => {
findRemoveAlertButton().vm.$emit('click');
expect(wrapper.emitted('update-alert')).toEqual([[false]]);
});
}); });
it('does emit an event to remove the alert', () => { describe('no agent installed', () => {
findRemoveAlertButton().vm.$emit('click'); beforeEach(async () => {
expect(wrapper.emitted('update-alert')).toEqual([[false]]); await createWrapper({ propsData: { policyAlert: true }, agentCount: 0 });
});
it('does render the "no agent" alert', () => {
expect(findNoAgentAlert().exists()).toBe(true);
});
it('does not render the "high volume" alert', async () => {
expect(findHighVolumeAlert().exists()).toBe(false);
});
}); });
}); });
}); });
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PolicyHelper do
let(:project) { create(:project, :repository, :public) }
let(:policy) do
Gitlab::Kubernetes::CiliumNetworkPolicy.new(
name: 'policy',
namespace: 'another',
selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
)
end
let(:environment) { create(:environment, project: project) }
describe '#policy_details' do
context 'when a new policy is being created' do
subject { helper.policy_details(project) }
it 'returns expected policy data' do
expect(subject).to match(
network_policies_endpoint: kind_of(String),
configure_agent_help_path: kind_of(String),
create_agent_help_path: kind_of(String),
environments_endpoint: kind_of(String),
project_path: project.full_path,
threat_monitoring_path: kind_of(String)
)
end
end
context 'when an existing policy is being edited' do
subject { helper.policy_details(project, policy, environment) }
it 'returns expected policy data' do
expect(subject).to match(
network_policies_endpoint: kind_of(String),
configure_agent_help_path: kind_of(String),
create_agent_help_path: kind_of(String),
environments_endpoint: kind_of(String),
project_path: project.full_path,
threat_monitoring_path: kind_of(String),
policy: policy.to_json,
environment_id: environment.id
)
end
end
end
end
...@@ -18938,6 +18938,9 @@ msgstr "" ...@@ -18938,6 +18938,9 @@ msgstr ""
msgid "NetworkPolicies|None selected" msgid "NetworkPolicies|None selected"
msgstr "" msgstr ""
msgid "NetworkPolicies|Please %{installLinkStart}install%{installLinkEnd} and %{configureLinkStart}configure a Kubernetes Agent for this project%{configureLinkEnd} to enable alerts."
msgstr ""
msgid "NetworkPolicies|Policies are a specification of how groups of pods are allowed to communicate with each other's network endpoints." msgid "NetworkPolicies|Policies are a specification of how groups of pods are allowed to communicate with each other's network endpoints."
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