summaryrefslogtreecommitdiff
path: root/lib/api/ci
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /lib/api/ci
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
downloadgitlab-ce-b76ae638462ab0f673e5915986070518dd3f9ad3.tar.gz
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'lib/api/ci')
-rw-r--r--lib/api/ci/helpers/runner.rb123
-rw-r--r--lib/api/ci/job_artifacts.rb143
-rw-r--r--lib/api/ci/jobs.rb206
-rw-r--r--lib/api/ci/pipelines.rb27
-rw-r--r--lib/api/ci/runner.rb22
-rw-r--r--lib/api/ci/triggers.rb148
-rw-r--r--lib/api/ci/variables.rb126
7 files changed, 776 insertions, 19 deletions
diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
new file mode 100644
index 00000000000..b9662b822fb
--- /dev/null
+++ b/lib/api/ci/helpers/runner.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ module Helpers
+ module Runner
+ include Gitlab::Utils::StrongMemoize
+
+ prepend_mod_with('API::Ci::Helpers::Runner') # rubocop: disable Cop/InjectEnterpriseEditionModule
+
+ JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'
+ JOB_TOKEN_PARAM = :token
+
+ def runner_registration_token_valid?
+ ActiveSupport::SecurityUtils.secure_compare(params[:token], Gitlab::CurrentSettings.runners_registration_token)
+ end
+
+ def runner_registrar_valid?(type)
+ Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
+ end
+
+ def authenticate_runner!
+ forbidden! unless current_runner
+
+ current_runner
+ .heartbeat(get_runner_details_from_request)
+ end
+
+ def get_runner_details_from_request
+ return get_runner_ip unless params['info'].present?
+
+ attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ .merge(get_runner_config_from_request)
+ .merge(get_runner_ip)
+ end
+
+ def get_runner_ip
+ { ip_address: ip_address }
+ end
+
+ def current_runner
+ token = params[:token]
+
+ if token
+ ::Gitlab::Database::LoadBalancing::RackMiddleware
+ .stick_or_unstick(env, :runner, token)
+ end
+
+ strong_memoize(:current_runner) do
+ ::Ci::Runner.find_by_token(token.to_s)
+ end
+ end
+
+ # HTTP status codes to terminate the job on GitLab Runner:
+ # - 403
+ def authenticate_job!(require_running: true)
+ job = current_job
+
+ # 404 is not returned here because we want to terminate the job if it's
+ # running. A 404 can be returned from anywhere in the networking stack which is why
+ # we are explicit about a 403, we should improve this in
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/327703
+ forbidden! unless job
+
+ forbidden! unless job_token_valid?(job)
+
+ forbidden!('Project has been deleted!') if job.project.nil? || job.project.pending_delete?
+ forbidden!('Job has been erased!') if job.erased?
+
+ if require_running
+ job_forbidden!(job, 'Job is not running') unless job.running?
+ end
+
+ job.runner&.heartbeat(get_runner_ip)
+
+ job
+ end
+
+ def current_job
+ id = params[:id]
+
+ if id
+ ::Gitlab::Database::LoadBalancing::RackMiddleware
+ .stick_or_unstick(env, :build, id)
+ end
+
+ strong_memoize(:current_job) do
+ ::Ci::Build.find_by_id(id)
+ end
+ end
+
+ def job_token_valid?(job)
+ token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
+ token && job.valid_token?(token)
+ end
+
+ def job_forbidden!(job, reason)
+ header 'Job-Status', job.status
+ forbidden!(reason)
+ end
+
+ def set_application_context
+ return unless current_job
+
+ Gitlab::ApplicationContext.push(
+ user: -> { current_job.user },
+ project: -> { current_job.project }
+ )
+ end
+
+ def track_ci_minutes_usage!(_build, _runner)
+ # noop: overridden in EE
+ end
+
+ private
+
+ def get_runner_config_from_request
+ { config: attributes_for_keys(%w(gpus), params.dig('info', 'config')) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb
new file mode 100644
index 00000000000..6431436b50d
--- /dev/null
+++ b/lib/api/ci/job_artifacts.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class JobArtifacts < ::API::Base
+ before { authenticate_non_get! }
+
+ feature_category :build_artifacts
+
+ # EE::API::Ci::JobArtifacts would override the following helpers
+ helpers do
+ def authorize_download_artifacts!
+ authorize_read_builds!
+ end
+ end
+
+ prepend_mod_with('API::Ci::JobArtifacts') # rubocop: disable Cop/InjectEnterpriseEditionModule
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Download the artifacts archive from a job' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the job'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get ':id/jobs/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_download_artifacts!
+
+ latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name])
+ authorize_read_job_artifacts!(latest_build)
+
+ present_carrierwave_file!(latest_build.artifacts_file)
+ end
+
+ desc 'Download a specific file from artifacts archive from a ref' do
+ detail 'This feature was introduced in GitLab 11.5'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the job'
+ requires :artifact_path, type: String, desc: 'Artifact path'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get ':id/jobs/artifacts/:ref_name/raw/*artifact_path',
+ format: false,
+ requirements: { ref_name: /.+/ } do
+ authorize_download_artifacts!
+
+ build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name])
+ authorize_read_job_artifacts!(build)
+
+ path = Gitlab::Ci::Build::Artifacts::Path
+ .new(params[:artifact_path])
+
+ bad_request! unless path.valid?
+
+ send_artifacts_entry(build.artifacts_file, path)
+ end
+
+ desc 'Download the artifacts archive from a job' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get ':id/jobs/:job_id/artifacts' do
+ authorize_download_artifacts!
+
+ build = find_build!(params[:job_id])
+ authorize_read_job_artifacts!(build)
+
+ present_carrierwave_file!(build.artifacts_file)
+ end
+
+ desc 'Download a specific file from artifacts archive' do
+ detail 'This feature was introduced in GitLab 10.0'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ requires :artifact_path, type: String, desc: 'Artifact path'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do
+ authorize_download_artifacts!
+
+ build = find_build!(params[:job_id])
+ authorize_read_job_artifacts!(build)
+
+ not_found! unless build.available_artifacts?
+
+ path = Gitlab::Ci::Build::Artifacts::Path
+ .new(params[:artifact_path])
+
+ bad_request! unless path.valid?
+
+ send_artifacts_entry(build.artifacts_file, path)
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success ::API::Entities::Ci::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ post ':id/jobs/:job_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = find_build!(params[:job_id])
+ authorize!(:update_build, build)
+ break not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: ::API::Entities::Ci::Job
+ end
+
+ desc 'Delete the artifacts files from a job' do
+ detail 'This feature was introduced in GitLab 11.9'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ delete ':id/jobs/:job_id/artifacts' do
+ authorize_destroy_artifacts!
+ build = find_build!(params[:job_id])
+ authorize!(:destroy_artifacts, build)
+
+ build.erase_erasable_artifacts!
+
+ status :no_content
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb
new file mode 100644
index 00000000000..eea1637c32a
--- /dev/null
+++ b/lib/api/ci/jobs.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class Jobs < ::API::Base
+ include PaginationParams
+ before { authenticate! }
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: ::CommitStatus::AVAILABLE_STATUSES,
+ coerce_with: ->(scope) {
+ case scope
+ when String
+ [scope]
+ when ::Hash
+ scope.values
+ when ::Array
+ scope
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
+ desc 'Get a projects jobs' do
+ success Entities::Ci::Job
+ end
+ params do
+ use :optional_scope
+ use :pagination
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ get ':id/jobs', feature_category: :continuous_integration do
+ authorize_read_builds!
+
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, :tags, pipeline: :project)
+ present paginate(builds), with: Entities::Ci::Job
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ desc 'Get a specific job of a project' do
+ success Entities::Ci::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ get ':id/jobs/:job_id', feature_category: :continuous_integration do
+ authorize_read_builds!
+
+ build = find_build!(params[:job_id])
+
+ present build, with: Entities::Ci::Job
+ end
+
+ # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ desc 'Get a trace of a specific job of a project'
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ get ':id/jobs/:job_id/trace', feature_category: :continuous_integration do
+ authorize_read_builds!
+
+ build = find_build!(params[:job_id])
+
+ authorize_read_build_trace!(build) if build
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ # The trace can be nil bu body method expects a string as an argument.
+ trace = build.trace.raw || ''
+ body trace
+ end
+
+ desc 'Cancel a specific job of a project' do
+ success Entities::Ci::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ post ':id/jobs/:job_id/cancel', feature_category: :continuous_integration do
+ authorize_update_builds!
+
+ build = find_build!(params[:job_id])
+ authorize!(:update_build, build)
+
+ build.cancel
+
+ present build, with: Entities::Ci::Job
+ end
+
+ desc 'Retry a specific build of a project' do
+ success Entities::Ci::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/jobs/:job_id/retry', feature_category: :continuous_integration do
+ authorize_update_builds!
+
+ build = find_build!(params[:job_id])
+ authorize!(:update_build, build)
+ break forbidden!('Job is not retryable') unless build.retryable?
+
+ build = ::Ci::Build.retry(build, current_user)
+
+ present build, with: Entities::Ci::Job
+ end
+
+ desc 'Erase job (remove artifacts and the trace)' do
+ success Entities::Ci::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/jobs/:job_id/erase', feature_category: :continuous_integration do
+ authorize_update_builds!
+
+ build = find_build!(params[:job_id])
+ authorize!(:erase_build, build)
+ break forbidden!('Job is not erasable!') unless build.erasable?
+
+ build.erase(erased_by: current_user)
+ present build, with: Entities::Ci::Job
+ end
+
+ desc 'Trigger an actionable job (manual, delayed, etc)' do
+ success Entities::Ci::JobBasic
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a Job'
+ end
+
+ post ":id/jobs/:job_id/play", feature_category: :continuous_integration do
+ authorize_read_builds!
+
+ job = find_job!(params[:job_id])
+
+ authorize!(:play_job, job)
+
+ bad_request!("Unplayable Job") unless job.playable?
+
+ job.play(current_user)
+
+ status 200
+
+ if job.is_a?(::Ci::Build)
+ present job, with: Entities::Ci::Job
+ else
+ present job, with: Entities::Ci::Bridge
+ end
+ end
+ end
+
+ resource :job do
+ desc 'Get current project using job token' do
+ success Entities::Ci::Job
+ end
+ route_setting :authentication, job_token_allowed: true
+ get '', feature_category: :continuous_integration do
+ validate_current_authenticated_job
+
+ present current_authenticated_job, with: Entities::Ci::Job
+ end
+ end
+
+ helpers do
+ # rubocop: disable CodeReuse/ActiveRecord
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def validate_current_authenticated_job
+ # current_authenticated_job will be nil if user is using
+ # a valid authentication (like PRIVATE-TOKEN) that is not CI_JOB_TOKEN
+ not_found!('Job') unless current_authenticated_job
+ end
+ end
+ end
+ end
+end
+
+API::Ci::Jobs.prepend_mod_with('API::Ci::Jobs')
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 339c0e779f9..4d6d38f2dce 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -44,7 +44,7 @@ module API
optional :ref, type: String, desc: 'The ref of pipelines'
optional :sha, type: String, desc: 'The sha of pipelines'
optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations'
- optional :name, type: String, desc: 'The name of the user who triggered pipelines'
+ optional :name, type: String, desc: '(deprecated) The name of the user who triggered pipelines'
optional :username, type: String, desc: 'The username of the user who triggered pipelines'
optional :updated_before, type: DateTime, desc: 'Return pipelines updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
optional :updated_after, type: DateTime, desc: 'Return pipelines updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
@@ -52,13 +52,14 @@ module API
desc: 'Order pipelines'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Sort pipelines'
+ optional :source, type: String, values: ::Ci::Pipeline.sources.keys
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
authorize! :read_build, user_project
pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute
- present paginate(pipelines), with: Entities::Ci::PipelineBasic
+ present paginate(pipelines), with: Entities::Ci::PipelineBasic, project: user_project
end
desc 'Create a new pipeline' do
@@ -78,12 +79,11 @@ module API
.merge(variables_attributes: params[:variables])
.except(:variables)
- new_pipeline = ::Ci::CreatePipelineService.new(user_project,
- current_user,
- pipeline_params)
- .execute(:api, ignore_skip_ci: true, save_on_errors: false)
+ response = ::Ci::CreatePipelineService.new(user_project, current_user, pipeline_params)
+ .execute(:api, ignore_skip_ci: true, save_on_errors: false)
+ new_pipeline = response.payload
- if new_pipeline.persisted?
+ if response.success?
present new_pipeline, with: Entities::Ci::Pipeline
else
render_validation_error!(new_pipeline)
@@ -188,6 +188,19 @@ module API
present pipeline.test_reports, with: TestReportEntity, details: true
end
+ desc 'Gets the test report summary for a given pipeline' do
+ detail 'This feature was introduced in GitLab 14.2'
+ success TestReportSummaryEntity
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id/test_report_summary' do
+ authorize! :read_build, pipeline
+
+ present pipeline.test_report_summary, with: TestReportSummaryEntity
+ end
+
desc 'Deletes a pipeline' do
detail 'This feature was introduced in GitLab 11.6'
http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 0bac6fe2054..aabcf34952c 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -3,12 +3,10 @@
module API
module Ci
class Runner < ::API::Base
- helpers ::API::Helpers::Runner
+ helpers ::API::Ci::Helpers::Runner
content_type :txt, 'text/plain'
- feature_category :runner
-
resource :runners do
desc 'Registers a new Runner' do
success Entities::Ci::RunnerRegistrationDetails
@@ -26,7 +24,7 @@ module API
optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: %q(List of Runner's tags)
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
- post '/' do
+ post '/', feature_category: :runner do
attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout])
.merge(get_runner_details_from_request)
@@ -59,7 +57,7 @@ module API
params do
requires :token, type: String, desc: %q(Runner's authentication token)
end
- delete '/' do
+ delete '/', feature_category: :runner do
authenticate_runner!
destroy_conditionally!(current_runner)
@@ -71,7 +69,7 @@ module API
params do
requires :token, type: String, desc: %q(Runner's authentication token)
end
- post '/verify' do
+ post '/verify', feature_category: :runner do
authenticate_runner!
status 200
body "200"
@@ -123,7 +121,7 @@ module API
formatter :build_json, ->(object, _) { object }
parser :build_json, ::Grape::Parser::Json
- post '/request' do
+ post '/request', feature_category: :continuous_integration do
authenticate_runner!
unless current_runner.active?
@@ -177,7 +175,7 @@ module API
end
optional :exit_code, type: Integer, desc: %q(Job's exit code)
end
- put '/:id' do
+ put '/:id', feature_category: :continuous_integration do
job = authenticate_job!
Gitlab::Metrics.add_event(:update_build)
@@ -204,7 +202,7 @@ module API
requires :id, type: Integer, desc: %q(Job's ID)
optional :token, type: String, desc: %q(Job's authentication token)
end
- patch '/:id/trace' do
+ patch '/:id/trace', feature_category: :continuous_integration do
job = authenticate_job!
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
@@ -249,7 +247,7 @@ module API
optional :artifact_type, type: String, desc: %q(The type of artifact),
default: 'archive', values: ::Ci::JobArtifact.file_types.keys
end
- post '/:id/artifacts/authorize' do
+ post '/:id/artifacts/authorize', feature_category: :build_artifacts do
not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse!
@@ -285,7 +283,7 @@ module API
default: 'zip', values: ::Ci::JobArtifact.file_formats.keys
optional :metadata, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact metadata to store (generated by Multipart middleware))
end
- post '/:id/artifacts' do
+ post '/:id/artifacts', feature_category: :build_artifacts do
not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse!
@@ -314,7 +312,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
optional :direct_download, default: false, type: Boolean, desc: %q(Perform direct download from remote storage instead of proxying artifacts)
end
- get '/:id/artifacts' do
+ get '/:id/artifacts', feature_category: :build_artifacts do
job = authenticate_job!(require_running: false)
present_carrierwave_file!(job.artifacts_file, supports_direct_download: params[:direct_download])
diff --git a/lib/api/ci/triggers.rb b/lib/api/ci/triggers.rb
new file mode 100644
index 00000000000..6a2b16e1568
--- /dev/null
+++ b/lib/api/ci/triggers.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class Triggers < ::API::Base
+ include PaginationParams
+
+ HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase
+
+ feature_category :continuous_integration
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Trigger a GitLab project pipeline' do
+ success Entities::Ci::Pipeline
+ end
+ params do
+ requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false
+ requires :token, type: String, desc: 'The unique token of trigger or job token'
+ optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
+ end
+ post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20758')
+
+ forbidden! if gitlab_pipeline_hook_request?
+
+ # validate variables
+ params[:variables] = params[:variables].to_h
+ unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ project = find_project(params[:id])
+ not_found! unless project
+
+ result = ::Ci::PipelineTriggerService.new(project, nil, params).execute
+ not_found! unless result
+
+ if result.error?
+ render_api_error!(result[:message], result[:http_status])
+ else
+ present result[:pipeline], with: Entities::Ci::Pipeline
+ end
+ end
+
+ desc 'Get triggers list' do
+ success Entities::Trigger
+ end
+ params do
+ use :pagination
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ get ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ triggers = user_project.triggers.includes(:trigger_requests)
+
+ present paginate(triggers), with: Entities::Trigger, current_user: current_user
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ desc 'Get specific trigger of a project' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ end
+ get ':id/triggers/:trigger_id' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ break not_found!('Trigger') unless trigger
+
+ present trigger, with: Entities::Trigger, current_user: current_user
+ end
+
+ desc 'Create a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :description, type: String, desc: 'The trigger description'
+ end
+ post ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.create(
+ declared_params(include_missing: false).merge(owner: current_user))
+
+ if trigger.valid?
+ present trigger, with: Entities::Trigger, current_user: current_user
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Update a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ optional :description, type: String, desc: 'The trigger description'
+ end
+ put ':id/triggers/:trigger_id' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ break not_found!('Trigger') unless trigger
+
+ authorize! :admin_trigger, trigger
+
+ if trigger.update(declared_params(include_missing: false))
+ present trigger, with: Entities::Trigger, current_user: current_user
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Delete a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ end
+ delete ':id/triggers/:trigger_id' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ break not_found!('Trigger') unless trigger
+
+ destroy_conditionally!(trigger)
+ end
+ end
+
+ helpers do
+ def gitlab_pipeline_hook_request?
+ request.get_header(HTTP_GITLAB_EVENT_HEADER) == WebHookService.hook_to_event(:pipeline_hooks)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ci/variables.rb b/lib/api/ci/variables.rb
new file mode 100644
index 00000000000..9c04d5e9923
--- /dev/null
+++ b/lib/api/ci/variables.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class Variables < ::API::Base
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize! :admin_build, user_project }
+
+ feature_category :pipeline_authoring
+
+ helpers ::API::Helpers::VariablesHelpers
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get project variables' do
+ success Entities::Ci::Variable
+ end
+ params do
+ use :pagination
+ end
+ get ':id/variables' do
+ variables = user_project.variables
+ present paginate(variables), with: Entities::Ci::Variable
+ end
+
+ desc 'Get a specific variable from a project' do
+ success Entities::Ci::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ get ':id/variables/:key' do
+ variable = find_variable(user_project, params)
+ not_found!('Variable') unless variable
+
+ present variable, with: Entities::Ci::Variable
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ desc 'Create a new variable in a project' do
+ success Entities::Ci::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ requires :value, type: String, desc: 'The value of the variable'
+ optional :protected, type: Boolean, desc: 'Whether the variable is protected'
+ optional :masked, type: Boolean, desc: 'Whether the variable is masked'
+ optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
+ optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
+ end
+ post ':id/variables' do
+ variable = ::Ci::ChangeVariableService.new(
+ container: user_project,
+ current_user: current_user,
+ params: { action: :create, variable_params: declared_params(include_missing: false) }
+ ).execute
+
+ if variable.valid?
+ present variable, with: Entities::Ci::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ desc 'Update an existing variable from a project' do
+ success Entities::Ci::Variable
+ end
+ params do
+ optional :key, type: String, desc: 'The key of the variable'
+ optional :value, type: String, desc: 'The value of the variable'
+ optional :protected, type: Boolean, desc: 'Whether the variable is protected'
+ optional :masked, type: Boolean, desc: 'Whether the variable is masked'
+ optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
+ optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
+ optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ put ':id/variables/:key' do
+ variable = find_variable(user_project, params)
+ not_found!('Variable') unless variable
+
+ variable = ::Ci::ChangeVariableService.new(
+ container: user_project,
+ current_user: current_user,
+ params: { action: :update, variable: variable, variable_params: declared_params(include_missing: false).except(:key, :filter) }
+ ).execute
+
+ if variable.valid?
+ present variable, with: Entities::Ci::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ desc 'Delete an existing variable from a project' do
+ success Entities::Ci::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ delete ':id/variables/:key' do
+ variable = find_variable(user_project, params)
+ not_found!('Variable') unless variable
+
+ ::Ci::ChangeVariableService.new(
+ container: user_project,
+ current_user: current_user,
+ params: { action: :destroy, variable: variable }
+ ).execute
+
+ no_content!
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end