diff options
Diffstat (limited to 'lib/api/v3')
-rw-r--r-- | lib/api/v3/builds.rb | 263 | ||||
-rw-r--r-- | lib/api/v3/deployments.rb | 41 | ||||
-rw-r--r-- | lib/api/v3/entities.rb | 55 | ||||
-rw-r--r-- | lib/api/v3/merge_request_diffs.rb | 43 | ||||
-rw-r--r-- | lib/api/v3/project_hooks.rb | 106 | ||||
-rw-r--r-- | lib/api/v3/services.rb | 68 |
6 files changed, 575 insertions, 1 deletions
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb new file mode 100644 index 00000000000..c8feba13527 --- /dev/null +++ b/lib/api/v3/builds.rb @@ -0,0 +1,263 @@ +module API + module V3 + class Builds < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + helpers do + params :optional_scope do + optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', + values: %w(pending running failed success canceled skipped), + coerce_with: ->(scope) { + if scope.is_a?(String) + [scope] + elsif scope.is_a?(Hashie::Mash) + scope.values + else + ['unknown'] + end + } + end + end + + desc 'Get a project builds' do + success ::API::V3::Entities::Build + end + params do + use :optional_scope + use :pagination + end + get ':id/builds' do + builds = user_project.builds.order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Get builds for a specific commit of a project' do + success ::API::V3::Entities::Build + end + params do + requires :sha, type: String, desc: 'The SHA id of a commit' + use :optional_scope + use :pagination + end + get ':id/repository/commits/:sha/builds' do + authorize_read_builds! + + return not_found! unless user_project.commit(params[:sha]) + + pipelines = user_project.pipelines.where(sha: params[:sha]) + builds = user_project.builds.where(pipeline: pipelines).order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Get a specific build of a project' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Download the artifacts file from build' do + detail 'This feature was introduced in GitLab 8.5' + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id/artifacts' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + present_artifacts!(build.artifacts_file) + end + + desc 'Download the artifacts file from build' 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 build' + end + get ':id/builds/artifacts/:ref_name/download', + requirements: { ref_name: /.+/ } do + authorize_read_builds! + + builds = user_project.latest_successful_builds_for(params[:ref_name]) + latest_build = builds.find_by!(name: params[:job]) + + present_artifacts!(latest_build.artifacts_file) + end + + # TODO: We should use `present_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 build of a project' + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id/trace' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" + content_type 'text/plain' + env['api.format'] = :binary + + trace = build.trace + body trace + end + + desc 'Cancel a specific build of a project' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/cancel' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + + build.cancel + + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Retry a specific build of a project' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/retry' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return forbidden!('Build is not retryable') unless build.retryable? + + build = Ci::Build.retry(build, current_user) + + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Erase build (remove artifacts and build trace)' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/erase' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return forbidden!('Build is not erasable!') unless build.erasable? + + build.erase(erased_by: current_user) + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + end + + desc 'Keep the artifacts to prevent them from being deleted' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/artifacts/keep' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return not_found!(build) unless build.artifacts? + + build.keep_artifacts! + + status 200 + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Trigger a manual build' do + success ::API::V3::Entities::Build + detail 'This feature was added in GitLab 8.11' + end + params do + requires :build_id, type: Integer, desc: 'The ID of a Build' + end + post ":id/builds/:build_id/play" do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + bad_request!("Unplayable Job") unless build.playable? + + build.play(current_user) + + status 200 + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + end + + helpers do + def get_build(id) + user_project.builds.find_by(id: id.to_i) + end + + def get_build!(id) + get_build(id) || not_found! + end + + def present_artifacts!(artifacts_file) + if !artifacts_file.file_storage? + redirect_to(build.artifacts_file.url) + elsif artifacts_file.exists? + present_file!(artifacts_file.path, artifacts_file.filename) + else + not_found! + end + end + + 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 + + def authorize_read_builds! + authorize! :read_build, user_project + end + + def authorize_update_builds! + authorize! :update_build, user_project + end + end + end + end +end diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb new file mode 100644 index 00000000000..545485fac0a --- /dev/null +++ b/lib/api/v3/deployments.rb @@ -0,0 +1,41 @@ +module API + # Deployments RESTfull API endpoints + class Deployments < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects do + desc 'Get all deployments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::V3::Deployments + end + params do + use :pagination + end + get ':id/deployments' do + authorize! :read_deployment, user_project + + present paginate(user_project.deployments), with: ::API::V3::Deployments + end + + desc 'Gets a specific deployment' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::V3::Deployments + end + params do + requires :deployment_id, type: Integer, desc: 'The deployment ID' + end + get ':id/deployments/:deployment_id' do + authorize! :read_deployment, user_project + + deployment = user_project.deployments.find(params[:deployment_id]) + + present deployment, with: ::API::V3::Deployments + end + end + end +end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 69853d33bec..832b4bdeb4f 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -81,7 +81,7 @@ module API expose :request_access_enabled expose :only_allow_merge_if_all_discussions_are_resolved - expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics + expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics end class ProjectWithAccess < Project @@ -195,6 +195,59 @@ module API class TriggerRequest < Grape::Entity expose :id, :variables end + + class Build < Grape::Entity + expose :id, :status, :stage, :name, :ref, :tag, :coverage + expose :created_at, :started_at, :finished_at + expose :user, with: ::API::Entities::User + expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? } + expose :commit, with: ::API::Entities::RepoCommit + expose :runner, with: ::API::Entities::Runner + expose :pipeline, with: ::API::Entities::PipelineBasic + end + + class BuildArtifactFile < Grape::Entity + expose :filename, :size + end + + class Deployment < Grape::Entity + expose :id, :iid, :ref, :sha, :created_at + expose :user, using: ::API::Entities::UserBasic + expose :environment, using: ::API::Entities::EnvironmentBasic + expose :deployable, using: Entities::Build + end + + class MergeRequestChanges < MergeRequest + expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _| + compare.raw_diffs(all_diffs: true).to_a + end + end + + class ProjectStatistics < Grape::Entity + expose :commit_count + expose :storage_size + expose :repository_size + expose :lfs_objects_size + expose :build_artifacts_size + end + + class ProjectService < Grape::Entity + expose :id, :title, :created_at, :updated_at, :active + expose :push_events, :issues_events, :merge_requests_events + expose :tag_push_events, :note_events, :build_events, :pipeline_events + # Expose serialized properties + expose :properties do |service, options| + field_names = service.fields. + select { |field| options[:include_passwords] || field[:type] != 'password' }. + map { |field| field[:name] } + service.properties.slice(*field_names) + end + end + + class ProjectHook < ::API::Entities::Hook + expose :project_id, :issues_events, :merge_requests_events + expose :note_events, :build_events, :pipeline_events, :wiki_page_events + end end end end diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb new file mode 100644 index 00000000000..a462803e26c --- /dev/null +++ b/lib/api/v3/merge_request_diffs.rb @@ -0,0 +1,43 @@ +module API + module V3 + # MergeRequestDiff API + class MergeRequestDiffs < Grape::API + before { authenticate! } + + resource :projects do + desc 'Get a list of merge request diff versions' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::MergeRequestDiff + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + end + + get ":id/merge_requests/:merge_request_id/versions" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff + end + + desc 'Get a single merge request diff version' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::MergeRequestDiffFull + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' + end + + get ":id/merge_requests/:merge_request_id/versions/:version_id" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull + end + end + end + end +end diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb new file mode 100644 index 00000000000..861b991b8e1 --- /dev/null +++ b/lib/api/v3/project_hooks.rb @@ -0,0 +1,106 @@ +module API + module V3 + class ProjectHooks < Grape::API + include PaginationParams + + before { authenticate! } + before { authorize_admin_project } + + helpers do + params :project_hook_properties do + requires :url, type: String, desc: "The URL to send the request to" + optional :push_events, type: Boolean, desc: "Trigger hook on push events" + optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" + optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" + optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" + optional :build_events, type: Boolean, desc: "Trigger hook on build events" + optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" + optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" + optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" + optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get project hooks' do + success ::API::V3::Entities::ProjectHook + end + params do + use :pagination + end + get ":id/hooks" do + hooks = paginate user_project.hooks + + present hooks, with: ::API::V3::Entities::ProjectHook + end + + desc 'Get a project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of a project hook' + end + get ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params[:hook_id]) + present hook, with: ::API::V3::Entities::ProjectHook + end + + desc 'Add hook to project' do + success ::API::V3::Entities::ProjectHook + end + params do + use :project_hook_properties + end + post ":id/hooks" do + hook = user_project.hooks.new(declared_params(include_missing: false)) + + if hook.save + present hook, with: ::API::V3::Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Update an existing project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: "The ID of the hook to update" + use :project_hook_properties + end + put ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params.delete(:hook_id)) + + if hook.update_attributes(declared_params(include_missing: false)) + present hook, with: ::API::V3::Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Deletes project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' + end + delete ":id/hooks/:hook_id" do + begin + present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook + rescue + # ProjectHook can raise Error if hook_id not found + not_found!("Error deleting hook #{params[:hook_id]}") + end + end + end + end + end +end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index af0a058f69b..d77185ffe5a 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -537,6 +537,23 @@ module API ] } + trigger_services = { + 'mattermost-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'slack-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Slack token' + } + ] + }.freeze + resource :projects do before { authenticate! } before { authorize_admin_project } @@ -567,6 +584,57 @@ module API render_api_error!('400 Bad Request', 400) end end + + desc 'Get the service settings for project' do + success Entities::ProjectService + end + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + get ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + present service, with: Entities::ProjectService, include_passwords: current_user.is_admin? + end + end + + trigger_services.each do |service_slug, settings| + helpers do + def chat_command_service(project, service_slug, params) + project.services.active.where(template: false).find do |service| + service.try(:token) == params[:token] && service.to_param == service_slug.underscore + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc "Trigger a slash command for #{service_slug}" do + detail 'Added in GitLab 8.13' + end + params do + settings.each do |setting| + requires setting[:name], type: setting[:type], desc: setting[:desc] + end + end + post ":id/services/#{service_slug.underscore}/trigger" do + project = find_project(params[:id]) + + # This is not accurate, but done to prevent leakage of the project names + not_found!('Service') unless project + + service = chat_command_service(project, service_slug, params) + result = service.try(:trigger, params) + + if result + status result[:status] || 200 + present result + else + not_found!('Service') + end + end + end end end end |