Commit 6b95281a authored by charlie ablett's avatar charlie ablett

Merge branch 'trace-parser' into 'master'

Parse requirements reports

See merge request gitlab-org/gitlab!33031
parents 04cc3758 71cbfa08
...@@ -39,7 +39,8 @@ module Ci ...@@ -39,7 +39,8 @@ module Ci
dotenv: '.env', dotenv: '.env',
cobertura: 'cobertura-coverage.xml', cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json', terraform: 'tfplan.json',
cluster_applications: 'gl-cluster-applications.json' cluster_applications: 'gl-cluster-applications.json',
requirements: 'requirements.json'
}.freeze }.freeze
INTERNAL_TYPES = { INTERNAL_TYPES = {
...@@ -71,7 +72,8 @@ module Ci ...@@ -71,7 +72,8 @@ module Ci
license_management: :raw, license_management: :raw,
license_scanning: :raw, license_scanning: :raw,
performance: :raw, performance: :raw,
terraform: :raw terraform: :raw,
requirements: :raw
}.freeze }.freeze
DOWNLOADABLE_TYPES = %w[ DOWNLOADABLE_TYPES = %w[
...@@ -90,6 +92,7 @@ module Ci ...@@ -90,6 +92,7 @@ module Ci
metrics metrics
performance performance
sast sast
requirements
].freeze ].freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
...@@ -182,7 +185,8 @@ module Ci ...@@ -182,7 +185,8 @@ module Ci
terraform: 18, # Transformed json terraform: 18, # Transformed json
accessibility: 19, accessibility: 19,
cluster_applications: 20, cluster_applications: 20,
secret_detection: 21 ## EE-specific secret_detection: 21, ## EE-specific
requirements: 22 ## EE-specific
} }
enum file_format: { enum file_format: {
......
...@@ -242,6 +242,8 @@ ...@@ -242,6 +242,8 @@
- 1 - 1
- - repository_update_remote_mirror - - repository_update_remote_mirror
- 1 - 1
- - requirements_management_process_requirements_reports
- 1
- - security_scans - - security_scans
- 2 - 2
- - self_monitoring_project_create - - self_monitoring_project_create
......
...@@ -116,6 +116,16 @@ module EE ...@@ -116,6 +116,16 @@ module EE
metrics_report metrics_report
end end
def collect_requirements_reports!(requirements_report)
return requirements_report unless project.feature_available?(:requirements)
each_report(::Ci::JobArtifact::REQUIREMENTS_REPORT_FILE_TYPES) do |file_type, blob, report_artifact|
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, requirements_report)
end
requirements_report
end
def retryable? def retryable?
!merge_train_pipeline? && super !merge_train_pipeline? && super
end end
......
...@@ -19,6 +19,7 @@ module EE ...@@ -19,6 +19,7 @@ module EE
SAST_REPORT_TYPES = %w[sast].freeze SAST_REPORT_TYPES = %w[sast].freeze
SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze
DAST_REPORT_TYPES = %w[dast].freeze DAST_REPORT_TYPES = %w[dast].freeze
REQUIREMENTS_REPORT_FILE_TYPES = %w[requirements].freeze
scope :project_id_in, ->(ids) { where(project_id: ids) } scope :project_id_in, ->(ids) { where(project_id: ids) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
......
...@@ -47,7 +47,8 @@ module EE ...@@ -47,7 +47,8 @@ module EE
performance: %i[merge_request_performance_metrics], performance: %i[merge_request_performance_metrics],
license_management: %i[license_scanning], license_management: %i[license_scanning],
license_scanning: %i[license_scanning], license_scanning: %i[license_scanning],
metrics: %i[metrics_reports] metrics: %i[metrics_reports],
requirements: %i[requirements]
}.freeze }.freeze
state_machine :status do state_machine :status do
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module RequirementsManagement module RequirementsManagement
class TestReport < ApplicationRecord class TestReport < ApplicationRecord
include Sortable include Sortable
include BulkInsertSafe
belongs_to :requirement, inverse_of: :test_reports belongs_to :requirement, inverse_of: :test_reports
belongs_to :author, inverse_of: :test_reports, class_name: 'User' belongs_to :author, inverse_of: :test_reports, class_name: 'User'
...@@ -14,6 +15,27 @@ module RequirementsManagement ...@@ -14,6 +15,27 @@ module RequirementsManagement
enum state: { passed: 1 } enum state: { passed: 1 }
scope :for_user_build, ->(user_id, build_id) { where(author_id: user_id, build_id: build_id) }
def self.persist_all_requirement_reports_as_passed(build)
reports = []
timestamp = Time.current
build.project.requirements.opened.select(:id).find_each do |requirement|
reports << new(
requirement_id: requirement.id,
# pipeline_reference will be removed:
# https://gitlab.com/gitlab-org/gitlab/-/issues/219999
pipeline_id: build.pipeline_id,
build_id: build.id,
author_id: build.user_id,
created_at: timestamp,
state: :passed
)
end
bulk_insert!(reports)
end
private private
def validate_pipeline_reference def validate_pipeline_reference
......
...@@ -396,6 +396,7 @@ module EE ...@@ -396,6 +396,7 @@ module EE
rule { requirements_available & reporter }.policy do rule { requirements_available & reporter }.policy do
enable :create_requirement enable :create_requirement
enable :create_requirement_test_report
enable :admin_requirement enable :admin_requirement
enable :update_requirement enable :update_requirement
end end
......
# frozen_string_literal: true
# This service collects all requirements reports from the CI job and creates a
# series of test report resources, one for each open requirement
module RequirementsManagement
class ProcessTestReportsService < BaseService
include Gitlab::Allowable
def initialize(build)
@build = build
end
def execute
return if test_report_already_generated?
return unless report.all_passed?
raise Gitlab::Access::AccessDeniedError unless can?(@build.user, :create_requirement_test_report, @build.project)
RequirementsManagement::TestReport.persist_all_requirement_reports_as_passed(@build)
end
private
def test_report_already_generated?
RequirementsManagement::TestReport.for_user_build(@build.user_id, @build.id).exists?
end
def report
::Gitlab::Ci::Reports::RequirementsManagement::Report.new.tap do |report|
@build.collect_requirements_reports!(report)
end
end
end
end
...@@ -643,6 +643,14 @@ ...@@ -643,6 +643,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: requirements_management_process_requirements_reports
:feature_category: :requirements_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: service_desk_email_receiver - :name: service_desk_email_receiver
:feature_category: :issue_tracking :feature_category: :issue_tracking
:has_external_dependencies: :has_external_dependencies:
......
...@@ -9,6 +9,7 @@ module EE ...@@ -9,6 +9,7 @@ module EE
::Ci::Minutes::EmailNotificationService.new(build.project.reset).execute if ::Gitlab.com? ::Ci::Minutes::EmailNotificationService.new(build.project.reset).execute if ::Gitlab.com?
StoreSecurityScansWorker.perform_async(build.id) StoreSecurityScansWorker.perform_async(build.id)
RequirementsManagement::ProcessRequirementsReportsWorker.perform_async(build.id)
super super
end end
......
# frozen_string_literal: true
module RequirementsManagement
class ProcessRequirementsReportsWorker
include ApplicationWorker
feature_category :requirements_management
idempotent!
def perform(build_id)
::Ci::Build.find_by_id(build_id).try do |build|
RequirementsManagement::ProcessTestReportsService.new(build).execute
end
end
end
end
---
title: Added CI parser for requirements reports
merge_request: 33031
author:
type: added
...@@ -16,7 +16,8 @@ module EE ...@@ -16,7 +16,8 @@ module EE
dast: ::Gitlab::Ci::Parsers::Security::Dast, dast: ::Gitlab::Ci::Parsers::Security::Dast,
sast: ::Gitlab::Ci::Parsers::Security::Sast, sast: ::Gitlab::Ci::Parsers::Security::Sast,
secret_detection: ::Gitlab::Ci::Parsers::Security::SecretDetection, secret_detection: ::Gitlab::Ci::Parsers::Security::SecretDetection,
metrics: ::Gitlab::Ci::Parsers::Metrics::Generic metrics: ::Gitlab::Ci::Parsers::Metrics::Generic,
requirements: ::Gitlab::Ci::Parsers::RequirementsManagement::Requirement
}) })
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module RequirementsManagement
class Requirement
RequirementParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
def parse!(json_data, report)
result = Gitlab::Json.parse!(json_data)
raise RequirementParserError, 'Invalid report format' unless result.is_a?(Hash)
result.each { |ref, state| report.add_requirement(ref, state) }
rescue JSON::ParserError
raise RequirementParserError, 'JSON parsing failed'
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module RequirementsManagement
class Report
attr_reader :requirements
def initialize
@requirements = {}
end
def add_requirement(key, value)
@requirements[key] = value
end
def all_passed?
@requirements['*'] == 'passed'
end
end
end
end
end
end
...@@ -121,5 +121,11 @@ FactoryBot.define do ...@@ -121,5 +121,11 @@ FactoryBot.define do
end end
end end
end end
trait :requirements_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :requirements, job: build)
end
end
end end
end end
...@@ -332,5 +332,15 @@ FactoryBot.define do ...@@ -332,5 +332,15 @@ FactoryBot.define do
artifact.file = fixture_file_upload(path, 'application/json') artifact.file = fixture_file_upload(path, 'application/json')
end end
end end
trait :requirements do
file_format { :raw }
file_type { :requirements }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/requirements_management/report.json'), 'application/json')
end
end
end end
end end
...@@ -407,6 +407,40 @@ RSpec.describe Ci::Build do ...@@ -407,6 +407,40 @@ RSpec.describe Ci::Build do
end end
end end
describe '#collect_requirements_reports!' do
subject { job.collect_requirements_reports!(requirements_report) }
let(:requirements_report) { Gitlab::Ci::Reports::RequirementsManagement::Report.new }
context 'when there is a requirements report' do
before do
create(:ee_ci_job_artifact, :requirements, job: job, project: job.project)
end
context 'when requirements are available' do
before do
stub_licensed_features(requirements: true)
end
it 'parses blobs and adds the results to the report' do
expect { subject }.to change { requirements_report.requirements.count }.from(0).to(1)
end
end
context 'when requirements are not available' do
before do
stub_licensed_features(requirements: false)
end
it 'does not parse requirements report' do
subject
expect(requirements_report.requirements.count).to eq(0)
end
end
end
end
describe '#retryable?' do describe '#retryable?' do
subject { build.retryable? } subject { build.retryable? }
......
...@@ -35,4 +35,41 @@ RSpec.describe RequirementsManagement::TestReport do ...@@ -35,4 +35,41 @@ RSpec.describe RequirementsManagement::TestReport do
end end
end end
end end
describe 'scopes' do
describe 'for_user_build' do
it "returns only test reports matching build's user and pipeline" do
user = create(:user)
build = create(:ci_build)
report1 = create(:test_report, author: user, build: build)
create(:test_report, author: user)
create(:test_report, build: build)
expect(described_class.for_user_build(user.id, build.id)).to match_array([report1])
end
end
end
describe '.persist_all_requirement_reports_as_passed' do
let_it_be(:project) { create(:project) }
let_it_be(:build) { create(:ee_ci_build, :requirements_report, project: project) }
subject { described_class.persist_all_requirement_reports_as_passed(build) }
it 'creates test report with passed status for each open requirement' do
requirement = create(:requirement, state: :opened, project: project)
create(:requirement, state: :opened)
create(:requirement, state: :archived, project: project)
expect { subject }.to change { RequirementsManagement::TestReport.count }.by(1)
reports = RequirementsManagement::TestReport.where(pipeline: build.pipeline)
expect(reports.size).to eq(1)
expect(reports.first).to have_attributes(
requirement: requirement,
author: build.user,
state: 'passed'
)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe RequirementsManagement::ProcessTestReportsService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:build) { create(:ee_ci_build, :requirements_report, project: project, user: user) }
describe '#execute' do
let_it_be(:requirement1) { create(:requirement, state: :opened, project: project) }
let_it_be(:requirement2) { create(:requirement, state: :opened, project: project) }
let_it_be(:requirement3) { create(:requirement, state: :archived, project: project) }
subject { described_class.new(build).execute }
before do
stub_licensed_features(requirements: true)
end
context 'when user can create requirements test reports' do
before do
project.add_reporter(user)
end
it 'creates new test report for each open requirement' do
expect(RequirementsManagement::TestReport).to receive(:persist_all_requirement_reports_as_passed).with(build).and_call_original
expect { subject }.to change { RequirementsManagement::TestReport.count }.by(2)
end
it 'does not create test report for the same pipeline and user twice' do
expect { subject }.to change { RequirementsManagement::TestReport.count }.by(2)
expect { subject }.not_to change { RequirementsManagement::TestReport }
end
context 'when build does not contain any requirements report' do
let(:build) { create(:ee_ci_build, project: project, user: user) }
it 'does not create any test report' do
expect { subject }.not_to change { RequirementsManagement::TestReport }
end
end
end
context 'when user is not allowed to create requirements test reports' do
it 'raises an exception' do
expect { subject }.to raise_exception(Gitlab::Access::AccessDeniedError)
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'resource with requirement permissions' do RSpec.shared_examples 'resource with requirement permissions' do
let(:all_permissions) { [:read_requirement, :create_requirement, :admin_requirement, :update_requirement, :destroy_requirement] } let(:all_permissions) do
[:read_requirement, :create_requirement, :admin_requirement,
:update_requirement, :destroy_requirement,
:create_requirement_test_report]
end
let(:manage_permissions) { all_permissions - [:destroy_requirement] } let(:manage_permissions) { all_permissions - [:destroy_requirement] }
let(:non_read_permissions) { all_permissions - [:read_requirement] } let(:non_read_permissions) { all_permissions - [:read_requirement] }
......
...@@ -62,5 +62,11 @@ RSpec.describe BuildFinishedWorker do ...@@ -62,5 +62,11 @@ RSpec.describe BuildFinishedWorker do
subject subject
end end
end end
it 'processes requirements reports' do
expect(RequirementsManagement::ProcessRequirementsReportsWorker).to receive(:perform_async)
subject
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe RequirementsManagement::ProcessRequirementsReportsWorker do
describe '#perform' do
context 'build exists' do
let(:build) { create(:ci_build) }
it 'processes requirements reports' do
service_double = instance_double(RequirementsManagement::ProcessTestReportsService, execute: true)
expect(RequirementsManagement::ProcessTestReportsService).to receive(:new).and_return(service_double)
described_class.new.perform(build.id)
end
end
context 'build does not exist' do
it 'does not store requirements reports' do
expect(RequirementsManagement::ProcessTestReportsService).not_to receive(:new)
described_class.new.perform(non_existing_record_id)
end
end
end
end
...@@ -14,7 +14,8 @@ module Gitlab ...@@ -14,7 +14,8 @@ module Gitlab
ALLOWED_KEYS = ALLOWED_KEYS =
%i[junit codequality sast secret_detection dependency_scanning container_scanning %i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif dast performance license_management license_scanning metrics lsif
dotenv cobertura terraform accessibility cluster_applications].freeze dotenv cobertura terraform accessibility cluster_applications
requirements].freeze
attributes ALLOWED_KEYS attributes ALLOWED_KEYS
...@@ -40,6 +41,7 @@ module Gitlab ...@@ -40,6 +41,7 @@ module Gitlab
validates :terraform, array_of_strings_or_string: true validates :terraform, array_of_strings_or_string: true
validates :accessibility, array_of_strings_or_string: true validates :accessibility, array_of_strings_or_string: true
validates :cluster_applications, array_of_strings_or_string: true validates :cluster_applications, array_of_strings_or_string: true
validates :requirements, array_of_strings_or_string: true
end end
end end
......
...@@ -38,7 +38,8 @@ describe Ci::RetryBuildService do ...@@ -38,7 +38,8 @@ describe Ci::RetryBuildService do
job_artifacts_codequality job_artifacts_metrics scheduled_at job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv job_artifacts_network_referee job_artifacts_dotenv
job_artifacts_cobertura needs job_artifacts_accessibility].freeze job_artifacts_cobertura needs job_artifacts_accessibility
job_artifacts_requirements].freeze
ignore_accessors = ignore_accessors =
%i[type lock_version target_url base_tags trace_sections %i[type lock_version target_url base_tags trace_sections
......
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