diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 2e5fa7923b32e4cdba197f31240300862dbc3c3a..f5c1379591acbb9a7da3f658d57006b6650ee27d 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -659,6 +659,8 @@ - 1 - - sbom_reports - 1 +- - sbom_sync_archived_status + - 1 - - sbom_sync_project_traversal_ids - 1 - - search_elastic_default_branch_changed diff --git a/ee/app/services/sbom/sync_archived_status_service.rb b/ee/app/services/sbom/sync_archived_status_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..2ee021a6b92ef99418031182b6c6a4b9ba091b23 --- /dev/null +++ b/ee/app/services/sbom/sync_archived_status_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Sbom + class SyncArchivedStatusService + include Gitlab::Utils::StrongMemoize + include Gitlab::ExclusiveLeaseHelpers + + BATCH_SIZE = 100 + LEASE_TTL = 1.hour + + def initialize(project_id) + @project_id = project_id + end + + def execute + return unless project + + in_lock(lease_key, ttl: LEASE_TTL) { update_archived_status } + end + + private + + attr_reader :project_id + + def update_archived_status + project.sbom_occurrences.each_batch(of: BATCH_SIZE) do |batch| + batch.update_all(archived: project.archived) + end + end + + def project + Project.find_by_id(project_id) + end + strong_memoize_attr :project + + def lease_key + "sync_sbom_occurrences_archived:projects:#{project_id}" + end + end +end diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index 750f9c80ffcc5a5e18885b65e8329da2f4764f6c..bc4e81c67154da1d2b00dab8d1371c8168d4cd54 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -1821,6 +1821,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: sbom_sync_archived_status + :worker_name: Sbom::SyncArchivedStatusWorker + :feature_category: :dependency_management + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: sbom_sync_project_traversal_ids :worker_name: Sbom::SyncProjectTraversalIdsWorker :feature_category: :dependency_management diff --git a/ee/app/workers/sbom/sync_archived_status_worker.rb b/ee/app/workers/sbom/sync_archived_status_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..ae42f83eabf36e60497cdc042b4182f7ff132244 --- /dev/null +++ b/ee/app/workers/sbom/sync_archived_status_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Sbom + class SyncArchivedStatusWorker + include Gitlab::EventStore::Subscriber + + data_consistency :always + feature_category :dependency_management + idempotent! + + def handle_event(event) + ::Sbom::SyncArchivedStatusService.new(event.data['project_id']).execute + end + end +end diff --git a/ee/config/feature_flags/gitlab_com_derisk/sync_project_archival_status_to_sbom_occurrences.yml b/ee/config/feature_flags/gitlab_com_derisk/sync_project_archival_status_to_sbom_occurrences.yml new file mode 100644 index 0000000000000000000000000000000000000000..f7d15342a58120e080419406f4bc0ea12e1249f2 --- /dev/null +++ b/ee/config/feature_flags/gitlab_com_derisk/sync_project_archival_status_to_sbom_occurrences.yml @@ -0,0 +1,9 @@ +--- +name: sync_project_archival_status_to_sbom_occurrences +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437636 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/143874 +rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17572 +milestone: '16.10' +group: group::threat insights +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/lib/ee/gitlab/event_store.rb b/ee/lib/ee/gitlab/event_store.rb index 1a9c42f4a2c54343f85e58365d1df95314c9dfbf..c38633214907a751c6d34c0afb2ca4bd45c7b753 100644 --- a/ee/lib/ee/gitlab/event_store.rb +++ b/ee/lib/ee/gitlab/event_store.rb @@ -63,6 +63,11 @@ def configure!(store) ::Feature.enabled?(:update_vuln_reads_traversal_ids_via_event, ::Group.find_by_id(event.data['group_id']), type: :gitlab_com_derisk) } + store.subscribe ::Sbom::SyncArchivedStatusWorker, to: ::Projects::ProjectArchivedEvent, + if: ->(event) do + project = ::Project.find_by_id(event.data['project_id']) + ::Feature.enabled?(:sync_project_archival_status_to_sbom_occurrences, project) + end end end end diff --git a/ee/spec/services/sbom/sync_archived_status_service_spec.rb b/ee/spec/services/sbom/sync_archived_status_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b26f5228e5397bc3c6463e010eca8d4eaa27eadb --- /dev/null +++ b/ee/spec/services/sbom/sync_archived_status_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sbom::SyncArchivedStatusService, feature_category: :dependency_management do + let_it_be(:project) { create(:project, :archived) } + let_it_be(:sbom_occurrence) { create(:sbom_occurrence, project: project) } + let_it_be(:sbom_occurrence_2) { create(:sbom_occurrence, project: project) } + + let(:project_id) { project.id } + + subject(:sync) { described_class.new(project_id).execute } + + it 'updates sbom_occurrences.archived' do + expect { sync }.to change { sbom_occurrence.reload.archived }.from(false).to(true) + end + + context 'when project does not exist with id' do + let(:project_id) { non_existing_record_id } + + it 'does not raise' do + expect { sync }.not_to raise_error + end + end + + context 'when lease is taken' do + include ExclusiveLeaseHelpers + + let_it_be(:other_project) { create(:project) } + + let(:lease_key) { "sync_sbom_occurrences_archived:projects:#{project_id}" } + let(:lease_ttl) { 1.hour } + + before do + stub_exclusive_lease_taken(lease_key, timeout: lease_ttl) + end + + it 'does not permit parallel execution on the same project' do + expect { sync }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + .and not_change { sbom_occurrence.reload.archived } + end + + it 'allows parallel execution on different projects' do + expect { described_class.new(other_project.id).execute }.not_to raise_error + end + end +end diff --git a/ee/spec/workers/sbom/sync_archived_status_worker_spec.rb b/ee/spec/workers/sbom/sync_archived_status_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..758ebda1b5eb4f7ab6f1a5ff3f9d7f6bf773e704 --- /dev/null +++ b/ee/spec/workers/sbom/sync_archived_status_worker_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sbom::SyncArchivedStatusWorker, feature_category: :dependency_management, type: :worker do + let_it_be(:project) { create(:project) } + let_it_be(:sbom_occurrence) { create(:sbom_occurrence, project: project) } + let_it_be(:sbom_occurrence_outside_project) { create(:sbom_occurrence) } + + let(:event) do + ::Projects::ProjectArchivedEvent.new(data: { + project_id: project.id, + namespace_id: project.namespace.id, + root_namespace_id: project.root_namespace.id + }) + end + + it_behaves_like 'worker with data consistency', described_class, data_consistency: :always + it_behaves_like 'subscribes to event' + + subject(:use_event) { consume_event(subscriber: described_class, event: event) } + + it 'updates sbom_occurrences archived status' do + project.update!(archived: true) + + expect { use_event }.to change { sbom_occurrence.reload.archived }.from(false).to(true) + .and not_change { sbom_occurrence_outside_project.reload.archived } + end + + context 'when sync_project_archival_status_to_sbom_occurrences is disabled' do + before do + stub_feature_flags(sync_project_archival_status_to_sbom_occurrences: false) + end + + it_behaves_like 'ignores the published event' + end +end