Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1bf106b172
commit
900c5cc840
|
|
@ -3,7 +3,7 @@
|
|||
module Clusters
|
||||
module Applications
|
||||
class Runner < ApplicationRecord
|
||||
VERSION = '0.30.0'
|
||||
VERSION = '0.31.0'
|
||||
|
||||
self.table_name = 'clusters_applications_runners'
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ This is the API documentation for [Helm](../../user/packages/helm_repository/ind
|
|||
WARNING:
|
||||
This API is used by the Helm-related package clients such as [Helm](https://helm.sh/)
|
||||
and [`helm-push`](https://github.com/chartmuseum/helm-push/#readme),
|
||||
and is generally not meant for manual consumption. This API is under development and is not ready
|
||||
for production use due to limited functionality.
|
||||
and is generally not meant for manual consumption.
|
||||
|
||||
For instructions on how to upload and install Helm packages from the GitLab
|
||||
Package Registry, see the [Helm registry documentation](../../user/packages/helm_repository/index.md).
|
||||
|
|
|
|||
|
|
@ -673,17 +673,19 @@ class definition to make it easy and clear:
|
|||
|
||||
```ruby
|
||||
module API
|
||||
class JobArtifacts < Grape::API::Instance
|
||||
# EE::API::JobArtifacts would override the following helpers
|
||||
helpers do
|
||||
def authorize_download_artifacts!
|
||||
authorize_read_builds!
|
||||
module Ci
|
||||
class JobArtifacts < Grape::API::Instance
|
||||
# EE::API::Ci::JobArtifacts would override the following helpers
|
||||
helpers do
|
||||
def authorize_download_artifacts!
|
||||
authorize_read_builds!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
API::JobArtifacts.prepend_mod_with('API::JobArtifacts')
|
||||
API::Ci::JobArtifacts.prepend_mod_with('API::Ci::JobArtifacts')
|
||||
```
|
||||
|
||||
And then we can follow regular object-oriented practices to override it:
|
||||
|
|
@ -691,14 +693,16 @@ And then we can follow regular object-oriented practices to override it:
|
|||
```ruby
|
||||
module EE
|
||||
module API
|
||||
module JobArtifacts
|
||||
extend ActiveSupport::Concern
|
||||
module Ci
|
||||
module JobArtifacts
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
prepended do
|
||||
helpers do
|
||||
def authorize_download_artifacts!
|
||||
super
|
||||
check_cross_project_pipelines_feature!
|
||||
prepended do
|
||||
helpers do
|
||||
def authorize_download_artifacts!
|
||||
super
|
||||
check_cross_project_pipelines_feature!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ GitLab allows [Owners](../user/permissions.md) to set a project's visibility as:
|
|||
|
||||
These visibility levels affect who can see the project in the public access directory (`/public`
|
||||
for your GitLab instance). For example, <https://gitlab.com/public>.
|
||||
You can control the visibility of individual features with
|
||||
[project feature settings](../user/permissions.md#project-features).
|
||||
|
||||
## Public projects
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18997) in GitLab 14.1.
|
||||
|
||||
WARNING:
|
||||
The Helm package registry for GitLab is under development and isn't ready for production use due to
|
||||
limited functionality.
|
||||
|
||||
Publish Helm packages in your project's Package Registry. Then install the
|
||||
packages whenever you need to use them as a dependency.
|
||||
|
||||
|
|
|
|||
|
|
@ -153,10 +153,14 @@ module API
|
|||
mount ::API::Branches
|
||||
mount ::API::BroadcastMessages
|
||||
mount ::API::BulkImports
|
||||
mount ::API::Ci::JobArtifacts
|
||||
mount ::API::Ci::Jobs
|
||||
mount ::API::Ci::Pipelines
|
||||
mount ::API::Ci::PipelineSchedules
|
||||
mount ::API::Ci::Runner
|
||||
mount ::API::Ci::Runners
|
||||
mount ::API::Ci::Triggers
|
||||
mount ::API::Ci::Variables
|
||||
mount ::API::Commits
|
||||
mount ::API::CommitStatuses
|
||||
mount ::API::ContainerRegistryEvent
|
||||
|
|
@ -190,8 +194,6 @@ module API
|
|||
mount ::API::IssueLinks
|
||||
mount ::API::Invitations
|
||||
mount ::API::Issues
|
||||
mount ::API::JobArtifacts
|
||||
mount ::API::Jobs
|
||||
mount ::API::Keys
|
||||
mount ::API::Labels
|
||||
mount ::API::Lint
|
||||
|
|
@ -268,14 +270,12 @@ module API
|
|||
mount ::API::Tags
|
||||
mount ::API::Templates
|
||||
mount ::API::Todos
|
||||
mount ::API::Triggers
|
||||
mount ::API::Unleash
|
||||
mount ::API::UsageData
|
||||
mount ::API::UsageDataQueries
|
||||
mount ::API::UsageDataNonSqlMetrics
|
||||
mount ::API::UserCounts
|
||||
mount ::API::Users
|
||||
mount ::API::Variables
|
||||
mount ::API::Version
|
||||
mount ::API::Wikis
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
module API
|
||||
module Ci
|
||||
class Runner < ::API::Base
|
||||
helpers ::API::Helpers::Runner
|
||||
helpers ::API::Ci::Helpers::Runner
|
||||
|
||||
content_type :txt, 'text/plain'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -8,7 +8,7 @@ module API
|
|||
before { authorize! :admin_group, user_group }
|
||||
feature_category :continuous_integration
|
||||
|
||||
helpers Helpers::VariablesHelpers
|
||||
helpers ::API::Helpers::VariablesHelpers
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a group'
|
||||
|
|
|
|||
|
|
@ -132,7 +132,10 @@ module API
|
|||
:forking_access_level,
|
||||
:issues_access_level,
|
||||
:lfs_enabled,
|
||||
:merge_pipelines_enabled,
|
||||
:merge_requests_access_level,
|
||||
:merge_requests_template,
|
||||
:merge_trains_enabled,
|
||||
:merge_method,
|
||||
:name,
|
||||
:only_allow_merge_if_all_discussions_are_resolved,
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Helpers
|
||||
module Runner
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
prepend_mod_with('API::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
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class JobArtifacts < ::API::Base
|
||||
before { authenticate_non_get! }
|
||||
|
||||
feature_category :build_artifacts
|
||||
|
||||
# EE::API::JobArtifacts would override the following helpers
|
||||
helpers do
|
||||
def authorize_download_artifacts!
|
||||
authorize_read_builds!
|
||||
end
|
||||
end
|
||||
|
||||
prepend_mod_with('API::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
|
||||
204
lib/api/jobs.rb
204
lib/api/jobs.rb
|
|
@ -1,204 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
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
|
||||
|
||||
API::Jobs.prepend_mod_with('API::Jobs')
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
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
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class Variables < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
before { authenticate! }
|
||||
before { authorize! :admin_build, user_project }
|
||||
|
||||
feature_category :pipeline_authoring
|
||||
|
||||
helpers 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
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Helpers::Runner do
|
||||
RSpec.describe API::Ci::Helpers::Runner do
|
||||
let(:ip_address) { '1.2.3.4' }
|
||||
let(:runner_class) do
|
||||
Class.new do
|
||||
include API::Helpers
|
||||
include API::Helpers::Runner
|
||||
include API::Ci::Helpers::Runner
|
||||
|
||||
attr_accessor :params
|
||||
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Helpers::Runner do
|
||||
let(:helper) { Class.new { include API::Helpers::Runner }.new }
|
||||
RSpec.describe API::Ci::Helpers::Runner do
|
||||
let(:helper) { Class.new { include API::Ci::Helpers::Runner }.new }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:env).and_return({})
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Jobs do
|
||||
RSpec.describe API::Ci::Jobs do
|
||||
include HttpBasicAuthHelpers
|
||||
include DependencyProxyHelpers
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ RSpec.describe API::Jobs do
|
|||
|
||||
context 'with job token authentication header' do
|
||||
include_context 'with auth headers' do
|
||||
let(:header) { { API::Helpers::Runner::JOB_TOKEN_HEADER => running_job.token } }
|
||||
let(:header) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => running_job.token } }
|
||||
end
|
||||
|
||||
it_behaves_like 'returns common job data' do
|
||||
|
|
@ -150,7 +150,7 @@ RSpec.describe API::Jobs do
|
|||
|
||||
context 'with non running job' do
|
||||
include_context 'with auth headers' do
|
||||
let(:header) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token } }
|
||||
let(:header) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token } }
|
||||
end
|
||||
|
||||
it_behaves_like 'returns unauthorized'
|
||||
|
|
@ -523,15 +523,13 @@ RSpec.describe API::Jobs do
|
|||
|
||||
context 'when artifacts are stored remotely' do
|
||||
let(:proxy_download) { false }
|
||||
let(:job) { create(:ci_build, pipeline: pipeline) }
|
||||
let(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
|
||||
|
||||
before do
|
||||
stub_artifacts_object_storage(proxy_download: proxy_download)
|
||||
end
|
||||
|
||||
let(:job) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
|
||||
|
||||
before do
|
||||
artifact
|
||||
job.reload
|
||||
|
||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||
|
|
@ -708,11 +706,7 @@ RSpec.describe API::Jobs do
|
|||
context 'with branch name containing slash' do
|
||||
before do
|
||||
pipeline.reload
|
||||
pipeline.update!(ref: 'improve/awesome',
|
||||
sha: project.commit('improve/awesome').sha)
|
||||
end
|
||||
|
||||
before do
|
||||
pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
|
||||
get_for_ref('improve/awesome')
|
||||
end
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
let(:job) { create(:ci_build, :pending, user: user, project: project, pipeline: pipeline, runner_id: runner.id) }
|
||||
let(:jwt) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
|
||||
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt } }
|
||||
let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
|
||||
let(:headers_with_token) { headers.merge(API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
|
||||
let(:file_upload) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
|
||||
let(:file_upload2) { fixture_file_upload('spec/fixtures/dk.png', 'image/gif') }
|
||||
|
||||
|
|
@ -398,7 +398,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
|
||||
context 'when using runners token' do
|
||||
it 'responds with forbidden' do
|
||||
upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
|
||||
upload_artifacts(file_upload, headers.merge(API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks do
|
|||
project: project, user: user, runner_id: runner.id, pipeline: pipeline)
|
||||
end
|
||||
|
||||
let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
|
||||
let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
|
||||
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
|
||||
let(:update_interval) { 10.seconds.to_i }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Triggers do
|
||||
RSpec.describe API::Ci::Triggers do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Variables do
|
||||
RSpec.describe API::Ci::Variables do
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let!(:project) { create(:project, creator_id: user.id) }
|
||||
|
|
@ -89,6 +89,8 @@ ci_cd_settings:
|
|||
- group_runners_enabled
|
||||
- merge_pipelines_enabled
|
||||
- merge_trains_enabled
|
||||
- merge_pipelines_enabled
|
||||
- merge_trains_enabled
|
||||
- auto_rollback_enabled
|
||||
remapped_attributes:
|
||||
default_git_depth: ci_default_git_depth
|
||||
|
|
|
|||
Loading…
Reference in New Issue