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>
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';
export default {
......@@ -7,6 +8,9 @@ export default {
ACTION: s__(
'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'),
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.`,
......@@ -15,12 +19,44 @@ export default {
components: {
GlAlert,
GlButton,
GlLink,
GlSprintf,
},
inject: {
configureAgentHelpPath: { type: String, default: '' },
createAgentHelpPath: { type: String, default: '' },
projectPath: { type: String, default: '' },
},
props: {
policyAlert: {
type: Boolean,
required: true,
policyAlert: { 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 {
<template>
<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 }}
</gl-alert>
<div
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
v-if="!policyAlert"
variant="link"
category="primary"
data-testid="add-alert"
:disabled="!isAgentInstalled"
@click="$emit('update-alert', !policyAlert)"
>
{{ $options.i18n.BUTTON_LABEL }}
......@@ -49,11 +112,11 @@ export default {
class="gl-w-full gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<span>
<gl-sprintf :message="$options.i18n.ACTION">
<gl-sprintf :message="$options.i18n.ACTION" data-testid="policy-alert-message">
<template #label="{ content }">
<label for="actionType" class="text-uppercase gl-font-lg gl-mr-4 gl-mb-0">{{
content
}}</label>
<label for="actionType" class="text-uppercase gl-font-lg gl-mr-4 gl-mb-0">
{{ content }}
</label>
</template>
<template #span="{ content }">
<span>{{ content }}</span>
......
query getAgentCount($projectPath: ID!) {
project(fullPath: $projectPath) {
clusterAgents {
count
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import PolicyEditorApp from './components/policy_editor/policy_editor.vue';
import createStore from './store';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.querySelector('#js-policy-builder-app');
const {
environmentsEndpoint,
configureAgentHelpPath,
createAgentHelpPath,
networkPoliciesEndpoint,
threatMonitoringPath,
policy,
projectPath,
environmentId,
} = el.dataset;
......@@ -31,6 +42,12 @@ export default () => {
return new Vue({
el,
apolloProvider,
provide: {
configureAgentHelpPath,
createAgentHelpPath,
projectPath,
},
store,
render(createElement) {
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)
- breadcrumb_title @policy_name
- 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),
environments_endpoint: project_environments_path(@project),
threat_monitoring_path: project_threat_monitoring_path(@project),
policy: @policy.to_json,
environment_id: @environment.id,
} }
#js-policy-builder-app{ data: policy_details }
- add_to_breadcrumbs s_("ThreatMonitoring|Threat Monitoring"), project_threat_monitoring_path(@project)
- breadcrumb_title s_("NetworkPolicies|New policy")
- 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),
environments_endpoint: project_environments_path(@project),
threat_monitoring_path: project_threat_monitoring_path(@project),
} }
#js-policy-builder-app{ data: policy_details }
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
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 getAgentCount from 'ee/threat_monitoring/graphql/queries/get_agent_count.query.graphql';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('PolicyAlertPicker component', () => {
let wrapper;
const createMockApolloProvider = ({ agentCount }) => {
const getAgentCountHandler = jest
.fn()
.mockResolvedValue({ data: { project: { clusterAgents: { count: agentCount } } } });
return createMockApollo([[getAgentCount, getAgentCountHandler]]);
};
const defaultProps = { policyAlert: false };
const findAddAlertButton = () => wrapper.find("[data-testid='add-alert']");
const findGlAlert = () => wrapper.find(GlAlert);
const findGlSprintf = () => wrapper.find(GlSprintf);
const findRemoveAlertButton = () => wrapper.find("[data-testid='remove-alert']");
const findAddAlertButton = () => wrapper.findByTestId('add-alert');
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 });
const createWrapper = ({ propsData = defaultProps } = {}) => {
wrapper = shallowMount(PolicyAlertPicker, {
wrapper = extendedWrapper(
shallowMount(PolicyAlertPicker, {
apolloProvider,
localVue,
propsData: {
...propsData,
},
});
provide: {
configureAgentHelpPath: '',
createAgentHelpPath: '',
projectPath: '',
},
}),
);
await wrapper.vm.$nextTick();
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('loading', () => {
describe('default state', () => {
beforeEach(() => {
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().props('disabled')).toBe(false);
});
it('does not render the "no agent" alert', () => {
expect(findNoAgentAlert().exists()).toBe(false);
});
});
describe('alert enabled', () => {
beforeEach(async () => {
createWrapper({ propsData: { policyAlert: true } });
});
it('does render the "high volume" alert', () => {
expect(findHighVolumeAlert().exists()).toBe(true);
});
it('does not render the "no agent" alert', () => {
expect(findNoAgentAlert().exists()).toBe(false);
});
});
});
describe('default state', () => {
describe('agent installed', () => {
beforeEach(async () => {
await createWrapper();
});
it('does not render the high volume warning', () => {
expect(findGlAlert().exists()).toBe(false);
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(findGlSprintf().exists()).toBe(false);
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]]);
});
});
describe('no agent installed', () => {
beforeEach(async () => {
await createWrapper({ agentCount: 0 });
});
it('does render the "no agent" alert', () => {
expect(findNoAgentAlert().exists()).toBe(true);
});
it('does render the disabled add alert button ', async () => {
expect(findAddAlertButton().exists()).toBe(true);
expect(findAddAlertButton().props('disabled')).toBe(true);
});
});
});
describe('alert enabled', () => {
beforeEach(() => {
createWrapper({ propsData: { policyAlert: 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 warning', () => {
expect(findGlAlert().exists()).toBe(true);
it('does render the "high volume" alert', () => {
expect(findHighVolumeAlert().exists()).toBe(true);
});
it('does render the alert message', () => {
expect(findGlSprintf().exists()).toBe(true);
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]]);
});
});
describe('no agent installed', () => {
beforeEach(async () => {
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 ""
msgid "NetworkPolicies|None selected"
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."
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