diff --git a/app/services/ci/components/usages/create_service.rb b/app/services/ci/components/usages/create_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..e6876ff01465e4a7c68e273c5401c2d6b5bc0188 --- /dev/null +++ b/app/services/ci/components/usages/create_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Ci + module Components + module Usages + class CreateService + def initialize(component, used_by_project:) + @component = component + @used_by_project = used_by_project + end + + def execute + component_usage = Ci::Catalog::Resources::Components::Usage.new( + component: component, + catalog_resource: component.catalog_resource, + project: component.project, + used_by_project_id: used_by_project.id + ) + + if component_usage.valid? + component_usage.save + + return ServiceResponse.success(message: 'Usage recorded') + end + + errors = component_usage.errors + + if errors.size == 1 && errors.first.type == :taken # Only unique validation failed + ServiceResponse.success(message: 'Usage already recorded for today') + else + ServiceResponse.error(message: errors.full_messages.join(', ')) + end + end + + private + + attr_reader :component, :used_by_project + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/component_usage.rb b/lib/gitlab/ci/pipeline/chain/component_usage.rb index 79b8ea2d9db0f7fd7e11e5658a18eac840b5ab3b..fa341a75c1be79f1c75898d2b513f39b656f3a9d 100644 --- a/lib/gitlab/ci/pipeline/chain/component_usage.rb +++ b/lib/gitlab/ci/pipeline/chain/component_usage.rb @@ -31,6 +31,8 @@ def track_event(component) value: component.resource_type_before_type_cast } ) + + ::Ci::Components::Usages::CreateService.new(component, used_by_project: project).execute end def included_catalog_components diff --git a/spec/lib/gitlab/ci/pipeline/chain/component_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/component_usage_spec.rb index f4291f4938f64f7a3c692fd090efd54d4102b31c..ce27de2db4361342a36642be9cc3a25931174568 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/component_usage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/component_usage_spec.rb @@ -40,6 +40,18 @@ let(:value) { 1 } # Default resource_type end + it 'creates a component usage record' do + expect { perform }.to change { Ci::Catalog::Resources::Components::Usage.count }.by(1) + end + + context 'when component usage has already been recorded', :freeze_time do + it 'does not create a component usage record' do + step.perform! + + expect { perform }.not_to change { Ci::Catalog::Resources::Components::Usage.count } + end + end + context 'when the FF `ci_track_catalog_component_usage` is disabled' do before do stub_feature_flags(ci_track_catalog_component_usage: false) @@ -50,6 +62,10 @@ perform end + + it 'does not create a component usage record' do + expect { perform }.not_to change { Ci::Catalog::Resources::Components::Usage.count } + end end end end diff --git a/spec/services/ci/components/usages/create_service_spec.rb b/spec/services/ci/components/usages/create_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..81c96e5598f4327b81565310c07b2d1ac46208d5 --- /dev/null +++ b/spec/services/ci/components/usages/create_service_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Components::Usages::CreateService, feature_category: :pipeline_composition do + let_it_be(:project) { create(:project) } + let_it_be(:component) { create(:ci_catalog_resource_component) } + + let(:service) { described_class.new(component, used_by_project: project) } + + describe '#execute' do + subject(:execute) { service.execute } + + it 'creates a usage record', :aggregate_failures do + expect { execute }.to change { Ci::Catalog::Resources::Components::Usage.count }.by(1) + expect(execute).to be_success + expect(execute.message).to eq('Usage recorded') + + usage = Ci::Catalog::Resources::Components::Usage.find_by(component: component) + + expect(usage.catalog_resource).to eq(component.catalog_resource) + expect(usage.project).to eq(component.project) + expect(usage.used_by_project_id).to eq(project.id) + end + + context 'when usage has already been recorded', :freeze_time do + it 'does not create a usage record' do + service.execute + + expect { execute }.not_to change { Ci::Catalog::Resources::Components::Usage.count } + expect(execute).to be_success + expect(execute.message).to eq('Usage already recorded for today') + end + end + + context 'when usage is invalid' do + it 'does not create a usage record and returns error' do + usage = instance_double( + Ci::Catalog::Resources::Components::Usage, valid?: false, + errors: instance_double(ActiveModel::Errors, full_messages: ['msg 1', 'msg 2'], size: 2)) + + allow(Ci::Catalog::Resources::Components::Usage).to receive(:new).and_return(usage) + + expect { execute }.not_to change { Ci::Catalog::Resources::Components::Usage.count } + expect(execute).to be_error + expect(execute.message).to eq('msg 1, msg 2') + end + end + end +end