diff options
Diffstat (limited to 'lib/api/ci')
-rw-r--r-- | lib/api/ci/helpers/runner.rb | 123 | ||||
-rw-r--r-- | lib/api/ci/job_artifacts.rb | 143 | ||||
-rw-r--r-- | lib/api/ci/jobs.rb | 206 | ||||
-rw-r--r-- | lib/api/ci/pipelines.rb | 27 | ||||
-rw-r--r-- | lib/api/ci/runner.rb | 22 | ||||
-rw-r--r-- | lib/api/ci/triggers.rb | 148 | ||||
-rw-r--r-- | lib/api/ci/variables.rb | 126 |
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 |