mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-25 16:03:48 +00:00
194 lines
6.5 KiB
Ruby
194 lines
6.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module Ci
|
|
class Lint
|
|
class Result
|
|
attr_reader :jobs, :merged_yaml, :errors, :warnings, :includes
|
|
|
|
def initialize(jobs:, merged_yaml:, errors:, warnings:, includes:)
|
|
@jobs = jobs
|
|
@merged_yaml = merged_yaml
|
|
@errors = errors
|
|
@warnings = warnings
|
|
@includes = includes
|
|
end
|
|
|
|
def valid?
|
|
@errors.empty?
|
|
end
|
|
|
|
def status
|
|
valid? ? :valid : :invalid
|
|
end
|
|
end
|
|
|
|
LOG_MAX_DURATION_THRESHOLD = 2.seconds
|
|
|
|
def initialize(project:, current_user:, sha: nil, verify_project_sha: true)
|
|
@project = project
|
|
@current_user = current_user
|
|
|
|
#
|
|
# We are deprecating these parameters because we'll replace the `legacy_validate` with the `validate` method.
|
|
#
|
|
# If the `sha` is not provided, the default is the project's head commit (or nil). In such case, we
|
|
# don't need to call `YamlProcessor.verify_project_sha!`, which prevents redundant calls to Gitaly.
|
|
@legacy_verify_project_sha = verify_project_sha && sha.present?
|
|
@legacy_sha = sha || project&.repository&.commit&.sha
|
|
end
|
|
|
|
# Our goal is to remove the `sha` dependency and the custom `YamlProcessor` usage from the CI linting logic.
|
|
# This legacy method is aimed to be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/543727.
|
|
def legacy_validate(content, dry_run: false, ref: project&.default_branch)
|
|
if dry_run
|
|
simulate_pipeline_creation(content, ref)
|
|
else
|
|
legacy_static_validation(content)
|
|
end
|
|
end
|
|
|
|
def validate(content, dry_run:, ref:)
|
|
if dry_run
|
|
simulate_pipeline_creation(content, ref)
|
|
else
|
|
lint_pipeline_creation(content, ref)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
attr_accessor :project, :current_user, :legacy_sha, :legacy_verify_project_sha
|
|
|
|
def simulate_pipeline_creation(content, ref)
|
|
pipeline = ::Ci::CreatePipelineService
|
|
.new(@project, @current_user, ref: ref)
|
|
.execute(:push, dry_run: true, content: content)
|
|
.payload
|
|
|
|
Result.new(
|
|
jobs: dry_run_convert_to_jobs(pipeline.stages),
|
|
merged_yaml: pipeline.config_metadata.try(:[], :merged_yaml),
|
|
errors: pipeline.error_messages.map(&:content),
|
|
warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content),
|
|
includes: pipeline.config_metadata.try(:[], :includes)
|
|
)
|
|
end
|
|
|
|
def lint_pipeline_creation(content, ref)
|
|
service = ::Ci::CreatePipelineService.new(@project, @current_user, ref: ref)
|
|
pipeline = service.execute(:push, linting: true, content: content).payload
|
|
|
|
Result.new(
|
|
jobs: yaml_processor_result_to_jobs(service.yaml_processor_result),
|
|
merged_yaml: pipeline.config_metadata.try(:[], :merged_yaml),
|
|
errors: pipeline.error_messages.map(&:content),
|
|
warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content),
|
|
includes: pipeline.config_metadata.try(:[], :includes)
|
|
)
|
|
end
|
|
|
|
def legacy_static_validation(content)
|
|
logger = build_logger
|
|
|
|
result = yaml_processor_result(content, logger)
|
|
|
|
Result.new(
|
|
jobs: yaml_processor_result_to_jobs(result),
|
|
merged_yaml: result.config_metadata[:merged_yaml],
|
|
errors: result.errors,
|
|
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT), # rubocop: disable CodeReuse/ActiveRecord
|
|
includes: result.config_metadata[:includes]
|
|
)
|
|
ensure
|
|
logger.commit(pipeline: ::Ci::Pipeline.new, caller: self.class.name)
|
|
end
|
|
|
|
def yaml_processor_result(content, logger)
|
|
logger.instrument(:yaml_process, once: true) do
|
|
Gitlab::Ci::YamlProcessor.new(content, project: project,
|
|
user: current_user,
|
|
ref: find_ref,
|
|
sha: legacy_sha,
|
|
verify_project_sha: legacy_verify_project_sha,
|
|
logger: logger).execute
|
|
end
|
|
end
|
|
|
|
def dry_run_convert_to_jobs(stages)
|
|
stages.reduce([]) do |jobs, stage|
|
|
jobs + stage.statuses.map do |job|
|
|
{
|
|
name: job.name,
|
|
stage: stage.name,
|
|
before_script: job.options[:before_script].to_a,
|
|
script: job.options[:script].to_a,
|
|
after_script: job.options[:after_script].to_a,
|
|
tag_list: (job.tag_list if job.is_a?(::Ci::Build)).to_a,
|
|
environment: job.options.dig(:environment, :name),
|
|
when: job.when,
|
|
allow_failure: job.allow_failure
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
def yaml_processor_result_to_jobs(result)
|
|
jobs = []
|
|
return jobs unless result&.valid?
|
|
|
|
result.stages.each do |stage_name|
|
|
result.builds.each do |job|
|
|
next unless job[:stage] == stage_name
|
|
|
|
jobs << {
|
|
name: job[:name],
|
|
stage: stage_name,
|
|
before_script: job.dig(:options, :before_script).to_a,
|
|
script: job.dig(:options, :script).to_a,
|
|
after_script: job.dig(:options, :after_script).to_a,
|
|
tag_list: job[:tag_list].to_a,
|
|
only: job[:only],
|
|
except: job[:except],
|
|
environment: job[:environment],
|
|
when: job[:when],
|
|
allow_failure: job[:allow_failure],
|
|
needs: job[:needs_attributes]
|
|
}
|
|
end
|
|
end
|
|
|
|
jobs
|
|
end
|
|
|
|
def build_logger
|
|
Gitlab::Ci::Pipeline::Logger.new(project: project) do |l|
|
|
l.log_when do |observations|
|
|
duration = observations['yaml_process_duration_s']
|
|
next false unless duration
|
|
|
|
duration >= LOG_MAX_DURATION_THRESHOLD
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_ref
|
|
ref = RefFinder.new(project).find_by_sha(legacy_sha)
|
|
|
|
return unless ref
|
|
|
|
allowed_to_write_ref?(ref) ? ref : nil
|
|
end
|
|
|
|
def allowed_to_write_ref?(ref)
|
|
access = Gitlab::UserAccess.new(current_user, container: project)
|
|
|
|
# We only check access for protected branches and not for protected tags
|
|
# because it's not possible to perform static validation on a protected tag
|
|
# as we initialize a new `Ci::Pipeline` in `Ci::Config` and do not pass `tag: true`.
|
|
access.can_update_branch?(ref)
|
|
end
|
|
end
|
|
end
|
|
end
|