Commit e9d19d59 authored by Thong Kuah's avatar Thong Kuah

Merge branch 'persist-network-alerts' into 'master'

feat: Allow gitlab agent to create alert

See merge request gitlab-org/gitlab!48515
parents 798e2797 acddf5d8
...@@ -135,6 +135,7 @@ class License < ApplicationRecord ...@@ -135,6 +135,7 @@ class License < ApplicationRecord
container_scanning container_scanning
coverage_fuzzing coverage_fuzzing
credentials_inventory credentials_inventory
cilium_alerts
custom_compliance_frameworks custom_compliance_frameworks
dast dast
dependency_scanning dependency_scanning
......
# frozen_string_literal: true
module AlertManagement
# Create alerts coming K8 through gitlab-agent
class NetworkAlertService < BaseService
include Gitlab::Utils::StrongMemoize
include ::IncidentManagement::Settings
MONITORING_TOOL = Gitlab::AlertManagement::Payload::MONITORING_TOOLS.fetch(:cilium)
def execute
return bad_request unless valid_payload_size?
# Not meant to run with a user, but with a agent
# See https://gitlab.com/gitlab-org/gitlab/-/issues/291986
process_request
return bad_request unless alert.persisted?
ServiceResponse.success
end
private
def valid_payload_size?
Gitlab::Utils::DeepSize.new(params).valid?
end
def process_request
if alert.persisted?
alert.register_new_event!
else
create_alert
end
end
def create_alert
if alert.save
alert.execute_services
SystemNoteService.create_new_alert(
alert,
MONITORING_TOOL
)
return
end
logger.warn(
message:
"Unable to create AlertManagement::Alert from #{MONITORING_TOOL}",
project_id: project.id,
alert_errors: alert.errors.messages
)
end
def logger
@logger ||= Gitlab::AppLogger
end
def alert
strong_memoize(:alert) { find_existing_alert || build_new_alert }
end
def find_existing_alert
AlertManagement::Alert.not_resolved.for_fingerprint(
project,
incoming_payload.gitlab_fingerprint
).first
end
def build_new_alert
AlertManagement::Alert.new(**incoming_payload.alert_params, domain: :threat_monitoring, ended_at: nil)
end
# https://gitlab.com/gitlab-org/gitlab/-/issues/292034
def incoming_payload
strong_memoize(:incoming_payload) do
Gitlab::AlertManagement::Payload.parse(
project,
params,
monitoring_tool: MONITORING_TOOL
)
end
end
def bad_request
ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
end
end
end
# frozen_string_literal: true
module EE
module API
module Internal
module Kubernetes
extend ActiveSupport::Concern
prepended do
namespace 'internal' do
namespace 'kubernetes' do
before { check_agent_token }
namespace 'modules/cilium/network_alert' do
desc 'POST network alerts' do
detail 'Creates network alert'
end
params do
requires :alert, type: Hash, desc: 'Alert details'
end
route_setting :authentication, cluster_agent_token_allowed: true
post '/' do
project = agent.project
not_found! if project.nil?
forbidden! unless project.feature_available?(:cilium_alerts)
result = ::AlertManagement::NetworkAlertService.new(agent.project, nil, params[:alert]).execute
status result.http_status
end
end
end
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Internal::Kubernetes do
let(:jwt_auth_headers) do
jwt_token = JWT.encode({ 'iss' => Gitlab::Kas::JWT_ISSUER }, Gitlab::Kas.secret, 'HS256')
{ Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => jwt_token }
end
let(:jwt_secret) { SecureRandom.random_bytes(Gitlab::Kas::SECRET_LENGTH) }
before do
allow(Gitlab::Kas).to receive(:secret).and_return(jwt_secret)
end
shared_examples 'authorization' do
context 'not authenticated' do
it 'returns 401' do
send_request(headers: { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => '' })
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'kubernetes_agent_internal_api feature flag disabled' do
before do
stub_feature_flags(kubernetes_agent_internal_api: false)
end
it 'returns 404' do
send_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
shared_examples 'agent authentication' do
it 'returns 403 if Authorization header not sent' do
send_request
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns 403 if Authorization is for non-existent agent' do
send_request(headers: { 'Authorization' => 'Bearer NONEXISTENT' })
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'POST /internal/kubernetes/modules/cilium/network_alert' do
def send_request(headers: {}, params: {})
post api('/internal/kubernetes/modules/cilium/network_alert'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
end
include_examples 'authorization'
include_examples 'agent authentication'
context 'is authenticated for an agent' do
let!(:agent_token) { create(:cluster_agent_token) }
let!(:agent) { agent_token.agent }
before do
stub_licensed_features(cilium_alerts: true)
end
let(:payload) do
{
alert: {
title: 'minimal',
message: 'network problem',
evalMatches: [{ value: 1, metric: 'Count', tags: {} }]
}
}
end
it 'returns no_content for valid alert payload' do
send_request(params: payload, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(AlertManagement::Alert.count).to eq(1)
expect(AlertManagement::Alert.all.first.project).to eq(agent.project)
expect(response).to have_gitlab_http_status(:ok)
end
context 'when payload is invalid' do
let(:payload) { { temp: {} } }
it 'returns bad request' do
send_request(params: payload, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when feature is not available' do
before do
stub_licensed_features(cilium_alerts: false)
end
it 'returns forbidden for non licensed project' do
send_request(params: payload, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::NetworkAlertService do
let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:environment) { create(:environment, project: project) }
describe '#execute' do
let(:service) { described_class.new(project, nil, payload) }
let(:tool) { Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:cilium] }
let(:starts_at) { Time.current.change(usec: 0) }
let(:ended_at) { nil }
let(:fingerprint) { 'test' }
let(:domain) { 'threat_monitoring' }
let(:incident_management_setting) { double(auto_close_incident?: auto_close_enabled) }
let(:auto_close_enabled) { true }
before do
allow(service).to receive(:incident_management_setting).and_return(
incident_management_setting
)
end
subject(:execute) { service.execute }
context 'with minimal payload' do
let(:payload_raw) do
{}.with_indifferent_access
end
let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
it_behaves_like 'creates an alert management alert'
end
context 'with valid payload' do
let(:payload_raw) do
{
title: 'alert title',
start_time: starts_at.rfc3339,
end_time: ended_at&.rfc3339,
severity: 'low',
monitoring_tool: tool,
service: 'GitLab Test Suite',
description: 'Very detailed description',
hosts: %w[1.1.1.1 2.2.2.2],
fingerprint: fingerprint,
gitlab_environment_name: environment.name
}.with_indifferent_access
end
let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
let(:last_alert_attributes) do
AlertManagement::Alert.last.attributes.except('id', 'iid', 'created_at', 'updated_at')
.with_indifferent_access
end
it_behaves_like 'creates an alert management alert'
it_behaves_like 'assigns the alert properties'
it 'creates a system note corresponding to alert creation' do
expect { subject }.to change(Note, :count).by(1)
expect(Note.last.note).to include(payload_raw.fetch(:monitoring_tool))
end
context 'when alert exists' do
let!(:alert) do
create(
:alert_management_alert,
project: project, domain: :threat_monitoring, fingerprint: Digest::SHA1.hexdigest(fingerprint)
)
end
it_behaves_like 'does not an create alert management alert'
end
context 'existing alert with same fingerprint' do
let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) }
let!(:alert) do
create(:alert_management_alert, domain: :threat_monitoring, project: project, fingerprint: fingerprint_sha)
end
it_behaves_like 'adds an alert management alert event'
context 'end time given' do
let(:ended_at) { Time.current.change(nsec: 0) }
context 'auto_close disabled' do
let(:auto_close_enabled) { false }
it 'does not resolve the alert' do
expect { subject }.not_to change { alert.reload.status }
end
it 'does not set the ended at' do
subject
expect(alert.reload.ended_at).to be_nil
end
it_behaves_like 'does not an create alert management alert'
end
end
context 'existing alert is resolved' do
let!(:alert) do
create(
:alert_management_alert,
:resolved,
project: project, domain: :threat_monitoring, fingerprint: fingerprint_sha
)
end
it_behaves_like 'creates an alert management alert'
it_behaves_like 'assigns the alert properties'
end
context 'existing alert is ignored' do
let!(:alert) do
create(
:alert_management_alert,
:ignored,
project: project, domain: :threat_monitoring, fingerprint: fingerprint_sha
)
end
it_behaves_like 'adds an alert management alert event'
end
context 'two existing alerts, one resolved one open' do
let!(:resolved_existing_alert) do
create(
:alert_management_alert,
:resolved,
project: project, fingerprint: fingerprint_sha
)
end
let!(:alert) do
create(:alert_management_alert, domain: :threat_monitoring, project: project, fingerprint: fingerprint_sha)
end
it_behaves_like 'adds an alert management alert event'
end
end
context 'end time given' do
let(:ended_at) { Time.current }
it_behaves_like 'creates an alert management alert'
it_behaves_like 'assigns the alert properties'
end
end
context 'with overlong payload' do
let(:deep_size_object) { instance_double(Gitlab::Utils::DeepSize, valid?: false) }
let(:payload) { ActionController::Parameters.new({}).permit! }
before do
allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object)
end
it_behaves_like 'does not process incident issues due to error', http_status: :bad_request
it_behaves_like 'does not an create alert management alert'
end
context 'error duing save' do
let(:payload_raw) do
{}.with_indifferent_access
end
let(:logger) { double(warn: {}) }
let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
it 'logs warning' do
expect_any_instance_of(AlertManagement::Alert).to receive(:save).and_return(false)
expect_any_instance_of(described_class).to receive(:logger).and_return(logger)
subject
expect(logger).to have_received(:warn).with(
hash_including(
message: "Unable to create AlertManagement::Alert from #{tool}",
project_id: project.id,
alert_errors: {}
)
)
end
end
end
end
...@@ -121,3 +121,5 @@ module API ...@@ -121,3 +121,5 @@ module API
end end
end end
end end
API::Internal::Kubernetes.prepend_if_ee('EE::API::Internal::Kubernetes')
...@@ -4,7 +4,8 @@ module Gitlab ...@@ -4,7 +4,8 @@ module Gitlab
module AlertManagement module AlertManagement
module Payload module Payload
MONITORING_TOOLS = { MONITORING_TOOLS = {
prometheus: 'Prometheus' prometheus: 'Prometheus',
cilium: 'Cilium'
}.freeze }.freeze
class << self class << self
......
...@@ -87,7 +87,7 @@ RSpec.describe API::Internal::Kubernetes do ...@@ -87,7 +87,7 @@ RSpec.describe API::Internal::Kubernetes do
end end
end end
describe "GET /internal/kubernetes/agent_info" do describe 'GET /internal/kubernetes/agent_info' do
def send_request(headers: {}, params: {}) def send_request(headers: {}, params: {})
get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
end end
......
...@@ -16,6 +16,38 @@ RSpec.shared_examples 'creates an alert management alert' do ...@@ -16,6 +16,38 @@ RSpec.shared_examples 'creates an alert management alert' do
end end
end end
# This shared_example requires the following variables:
# - last_alert_attributes, last created alert
# - project, project that alert created
# - payload_raw, hash representation of payload
# - environment, project's environment
# - fingerprint, fingerprint hash
RSpec.shared_examples 'assigns the alert properties' do
it 'ensures that created alert has all data properly assigned' do
subject
expect(last_alert_attributes).to match(
project_id: project.id,
title: payload_raw.fetch(:title),
started_at: Time.zone.parse(payload_raw.fetch(:start_time)),
severity: payload_raw.fetch(:severity),
status: AlertManagement::Alert.status_value(:triggered),
events: 1,
domain: domain,
hosts: payload_raw.fetch(:hosts),
payload: payload_raw.with_indifferent_access,
issue_id: nil,
description: payload_raw.fetch(:description),
monitoring_tool: payload_raw.fetch(:monitoring_tool),
service: payload_raw.fetch(:service),
fingerprint: Digest::SHA1.hexdigest(fingerprint),
environment_id: environment.id,
ended_at: nil,
prometheus_alert_id: nil
)
end
end
RSpec.shared_examples 'does not an create alert management alert' do RSpec.shared_examples 'does not an create alert management alert' do
it 'does not create alert' do it 'does not create alert' do
expect { subject }.not_to change(AlertManagement::Alert, :count) expect { subject }.not_to change(AlertManagement::Alert, :count)
......
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