# frozen_string_literal: true module Ci class CreateJobArtifactsService < ::BaseService include Gitlab::Utils::UsageData ArtifactsExistError = Class.new(StandardError) LSIF_ARTIFACT_TYPE = 'lsif' OBJECT_STORAGE_ERRORS = [ Errno::EIO, Google::Apis::ServerError, Signet::RemoteServerError ].freeze def initialize(job) @job = job @project = job.project end def authorize(artifact_type:, filesize: nil) result = validate_requirements(artifact_type: artifact_type, filesize: filesize) return result unless result[:status] == :success headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size(artifact_type)) if lsif?(artifact_type) headers[:ProcessLsif] = true track_usage_event('i_source_code_code_intelligence', project.id) end success(headers: headers) end def execute(artifacts_file, params, metadata_file: nil) result = validate_requirements(artifact_type: params[:artifact_type], filesize: artifacts_file.size) return result unless result[:status] == :success return success if sha256_matches_existing_artifact?(params[:artifact_type], artifacts_file) artifact, artifact_metadata = build_artifact(artifacts_file, params, metadata_file) result = parse_artifact(artifact) return result unless result[:status] == :success persist_artifact(artifact, artifact_metadata, params) end private attr_reader :job, :project def validate_requirements(artifact_type:, filesize:) return forbidden_type_error(artifact_type) if forbidden_type?(artifact_type) return too_large_error if too_large?(artifact_type, filesize) success end def forbidden_type?(type) lsif?(type) && !code_navigation_enabled? end def too_large?(type, size) size > max_size(type) if size end def code_navigation_enabled? Feature.enabled?(:code_navigation, project, default_enabled: true) end def lsif?(type) type == LSIF_ARTIFACT_TYPE end def max_size(type) Ci::JobArtifact.max_artifact_size(type: type, project: project) end def forbidden_type_error(type) error("#{type} artifacts are forbidden", :forbidden) end def too_large_error error('file size has reached maximum size limit', :payload_too_large) end def build_artifact(artifacts_file, params, metadata_file) expire_in = params['expire_in'] || Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in artifact = Ci::JobArtifact.new( job_id: job.id, project: project, file: artifacts_file, file_type: params[:artifact_type], file_format: params[:artifact_format], file_sha256: artifacts_file.sha256, expire_in: expire_in) artifact_metadata = if metadata_file Ci::JobArtifact.new( job_id: job.id, project: project, file: metadata_file, file_type: :metadata, file_format: :gzip, file_sha256: metadata_file.sha256, expire_in: expire_in) end [artifact, artifact_metadata] end def parse_artifact(artifact) unless Feature.enabled?(:ci_synchronous_artifact_parsing, project, default_enabled: true) return success end case artifact.file_type when 'dotenv' then parse_dotenv_artifact(artifact) when 'cluster_applications' then parse_cluster_applications_artifact(artifact) else success end end def persist_artifact(artifact, artifact_metadata, params) Ci::JobArtifact.transaction do artifact.save! artifact_metadata&.save! # NOTE: The `artifacts_expire_at` column is already deprecated and to be removed in the near future. job.update_column(:artifacts_expire_at, artifact.expire_at) end success rescue ActiveRecord::RecordNotUnique => error track_exception(error, params) error('another artifact of the same type already exists', :bad_request) rescue *OBJECT_STORAGE_ERRORS => error track_exception(error, params) error(error.message, :service_unavailable) rescue => error track_exception(error, params) error(error.message, :bad_request) end def sha256_matches_existing_artifact?(artifact_type, artifacts_file) existing_artifact = job.job_artifacts.find_by_file_type(artifact_type) return false unless existing_artifact existing_artifact.file_sha256 == artifacts_file.sha256 end def track_exception(error, params) Gitlab::ErrorTracking.track_exception(error, job_id: job.id, project_id: job.project_id, uploading_type: params[:artifact_type] ) end def parse_dotenv_artifact(artifact) Ci::ParseDotenvArtifactService.new(project, current_user).execute(artifact) end def parse_cluster_applications_artifact(artifact) Clusters::ParseClusterApplicationsArtifactService.new(job, job.user).execute(artifact) end end end