Commit 109956a6 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'project_api' into 'master'

Improve Project API

Closes #49938, #40892, and #35992

See merge request gitlab-org/gitlab-ce!28327
parents 6e106f66 2615265e
# frozen_string_literal: true
# Add methods used by the projects API
module ProjectAPICompatibility
extend ActiveSupport::Concern
def build_git_strategy=(value)
write_attribute(:build_allow_git_fetch, value == 'fetch')
end
def auto_devops_enabled=(value)
self.build_auto_devops if self.auto_devops&.enabled.nil?
self.auto_devops.update! enabled: value
end
def auto_devops_deploy_strategy=(value)
self.build_auto_devops if self.auto_devops&.enabled.nil?
self.auto_devops.update! deploy_strategy: value
end
end
......@@ -9,32 +9,70 @@ require 'gitlab/utils'
module ProjectFeaturesCompatibility
extend ActiveSupport::Concern
# TODO: remove in API v5, replaced by *_access_level
def wiki_enabled=(value)
write_feature_attribute(:wiki_access_level, value)
write_feature_attribute_boolean(:wiki_access_level, value)
end
# TODO: remove in API v5, replaced by *_access_level
def builds_enabled=(value)
write_feature_attribute(:builds_access_level, value)
write_feature_attribute_boolean(:builds_access_level, value)
end
# TODO: remove in API v5, replaced by *_access_level
def merge_requests_enabled=(value)
write_feature_attribute(:merge_requests_access_level, value)
write_feature_attribute_boolean(:merge_requests_access_level, value)
end
# TODO: remove in API v5, replaced by *_access_level
def issues_enabled=(value)
write_feature_attribute(:issues_access_level, value)
write_feature_attribute_boolean(:issues_access_level, value)
end
# TODO: remove in API v5, replaced by *_access_level
def snippets_enabled=(value)
write_feature_attribute(:snippets_access_level, value)
write_feature_attribute_boolean(:snippets_access_level, value)
end
def repository_access_level=(value)
write_feature_attribute_string(:repository_access_level, value)
end
def wiki_access_level=(value)
write_feature_attribute_string(:wiki_access_level, value)
end
def builds_access_level=(value)
write_feature_attribute_string(:builds_access_level, value)
end
def merge_requests_access_level=(value)
write_feature_attribute_string(:merge_requests_access_level, value)
end
def issues_access_level=(value)
write_feature_attribute_string(:issues_access_level, value)
end
def snippets_access_level=(value)
write_feature_attribute_string(:snippets_access_level, value)
end
private
def write_feature_attribute(field, value)
def write_feature_attribute_boolean(field, value)
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
write_feature_attribute_raw(field, access_level)
end
def write_feature_attribute_string(field, value)
access_level = ProjectFeature.access_level_from_str(value)
write_feature_attribute_raw(field, access_level)
end
def write_feature_attribute_raw(field, value)
build_project_feature unless project_feature
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend
project_feature.__send__(:write_attribute, field, value) # rubocop:disable GitlabSecurity/PublicSend
end
end
......@@ -15,6 +15,7 @@ class Project < ApplicationRecord
include CaseSensitivity
include TokenAuthenticatable
include ValidAttribute
include ProjectAPICompatibility
include ProjectFeaturesCompatibility
include SelectForProjectAuthorization
include Presentable
......
......@@ -24,6 +24,12 @@ class ProjectFeature < ApplicationRecord
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
STRING_OPTIONS = HashWithIndifferentAccess.new({
'disabled' => DISABLED,
'private' => PRIVATE,
'enabled' => ENABLED,
'public' => PUBLIC
}).freeze
class << self
def access_level_attribute(feature)
......@@ -45,6 +51,14 @@ class ProjectFeature < ApplicationRecord
PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST)
end
def access_level_from_str(level)
STRING_OPTIONS.fetch(level)
end
def str_from_access_level(level)
STRING_OPTIONS.key(level)
end
private
def ensure_feature!(feature)
......@@ -83,6 +97,10 @@ class ProjectFeature < ApplicationRecord
public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
end
def string_access_level(feature)
ProjectFeature.str_from_access_level(access_level(feature))
end
def builds_enabled?
builds_access_level > DISABLED
end
......
---
title: Improve Project API
merge_request: 28327
author: Mathieu Parent
type: added
This diff is collapsed.
......@@ -201,6 +201,7 @@ module API
# MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
projects_relation.preload(:project_feature, :route)
.preload(:import_state, :tags)
.preload(:auto_devops)
.preload(namespace: [:route, :owner])
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -247,12 +248,20 @@ module API
expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
# TODO: remove in API v5, replaced by *_access_level
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) }
expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) }
expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) }
expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) }
expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) }
expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) }
expose :shared_runners_enabled
expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
......@@ -267,6 +276,12 @@ module API
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :ci_default_git_depth
expose :public_builds, as: :public_jobs
expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options|
project.build_allow_git_fetch ? 'fetch' : 'clone'
end
expose :build_timeout
expose :auto_cancel_pending_pipelines
expose :build_coverage_regex
expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links, options)
......@@ -280,6 +295,10 @@ module API
options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
}
expose :external_authorization_classification_label
expose :auto_devops_enabled?, as: :auto_devops_enabled
expose :auto_devops_deploy_strategy do |project, options|
project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
end
# rubocop: disable CodeReuse/ActiveRecord
def self.preload_relation(projects_relation, options = {})
......@@ -289,6 +308,7 @@ module API
# MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
super(projects_relation).preload(:group)
.preload(:ci_cd_settings)
.preload(:auto_devops)
.preload(project_group_links: { group: :route },
fork_network: :root_project,
fork_network_member: :forked_from_project,
......
......@@ -8,12 +8,26 @@ module API
params :optional_project_params_ce do
optional :description, type: String, desc: 'The description of the project'
optional :build_git_strategy, type: String, values: %w(fetch clone), desc: 'The Git strategy. Defaults to `fetch`'
optional :build_timeout, type: Integer, desc: 'Build timeout'
optional :auto_cancel_pending_pipelines, type: String, values: %w(disabled enabled), desc: 'Auto-cancel pending pipelines'
optional :build_coverage_regex, type: String, desc: 'Test coverage parsing'
optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`'
# TODO: remove in API v5, replaced by *_access_level
optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled'
optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
optional :issues_access_level, type: String, values: %w(disabled private enabled), desc: 'Issues access level. One of `disabled`, `private` or `enabled`'
optional :repository_access_level, type: String, values: %w(disabled private enabled), desc: 'Repository access level. One of `disabled`, `private` or `enabled`'
optional :merge_requests_access_level, type: String, values: %w(disabled private enabled), desc: 'Merge requests access level. One of `disabled`, `private` or `enabled`'
optional :wiki_access_level, type: String, values: %w(disabled private enabled), desc: 'Wiki access level. One of `disabled`, `private` or `enabled`'
optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`'
optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
......@@ -30,6 +44,8 @@ module API
optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning'
optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled'
optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy'
end
params :optional_project_params_ee do
......@@ -48,15 +64,20 @@ module API
def self.update_params_at_least_one_of
[
:jobs_enabled,
:resolve_outdated_diff_discussions,
:auto_devops_enabled,
:auto_devops_deploy_strategy,
:auto_cancel_pending_pipelines,
:build_coverage_regex,
:build_git_strategy,
:build_timeout,
:builds_access_level,
:ci_config_path,
:container_registry_enabled,
:default_branch,
:description,
:issues_enabled,
:issues_access_level,
:lfs_enabled,
:merge_requests_enabled,
:merge_requests_access_level,
:merge_method,
:name,
:only_allow_merge_if_all_discussions_are_resolved,
......@@ -64,14 +85,24 @@ module API
:path,
:printing_merge_request_link_enabled,
:public_builds,
:repository_access_level,
:request_access_enabled,
:resolve_outdated_diff_discussions,
:shared_runners_enabled,
:snippets_enabled,
:snippets_access_level,
:tag_list,
:visibility,
:wiki_enabled,
:wiki_access_level,
:avatar,
:external_authorization_classification_label
:external_authorization_classification_label,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,
:jobs_enabled,
:merge_requests_enabled,
:wiki_enabled,
:jobs_enabled,
:snippets_enabled
]
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe ProjectAPICompatibility do
let(:project) { create(:project) }
# git_strategy
it "converts build_git_strategy=fetch to build_allow_git_fetch=true" do
project.update!(build_git_strategy: 'fetch')
expect(project.build_allow_git_fetch).to eq(true)
end
it "converts build_git_strategy=clone to build_allow_git_fetch=false" do
project.update!(build_git_strategy: 'clone')
expect(project.build_allow_git_fetch).to eq(false)
end
# auto_devops_enabled
it "converts auto_devops_enabled=false to auto_devops_enabled?=false" do
expect(project.auto_devops_enabled?).to eq(true)
project.update!(auto_devops_enabled: false)
expect(project.auto_devops_enabled?).to eq(false)
end
it "converts auto_devops_enabled=true to auto_devops_enabled?=true" do
expect(project.auto_devops_enabled?).to eq(true)
project.update!(auto_devops_enabled: true)
expect(project.auto_devops_enabled?).to eq(true)
end
# auto_devops_deploy_strategy
it "converts auto_devops_deploy_strategy=timed_incremental to auto_devops.deploy_strategy=timed_incremental" do
expect(project.auto_devops).to be_nil
project.update!(auto_devops_deploy_strategy: 'timed_incremental')
expect(project.auto_devops.deploy_strategy).to eq('timed_incremental')
end
end
......@@ -4,7 +4,8 @@ require 'spec_helper'
describe ProjectFeaturesCompatibility do
let(:project) { create(:project) }
let(:features) { %w(issues wiki builds merge_requests snippets) }
let(:features_except_repository) { %w(issues wiki builds merge_requests snippets) }
let(:features) { features_except_repository + ['repository'] }
# We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
# All those fields got moved to a new table called project_feature and are now integers instead of booleans
......@@ -12,30 +13,37 @@ describe ProjectFeaturesCompatibility do
# So we can keep it compatible
it "converts fields from 'true' to ProjectFeature::ENABLED" do
features.each do |feature|
features_except_repository.each do |feature|
project.update_attribute("#{feature}_enabled".to_sym, "true")
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
end
end
it "converts fields from 'false' to ProjectFeature::DISABLED" do
features.each do |feature|
features_except_repository.each do |feature|
project.update_attribute("#{feature}_enabled".to_sym, "false")
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
end
end
it "converts fields from true to ProjectFeature::ENABLED" do
features.each do |feature|
features_except_repository.each do |feature|
project.update_attribute("#{feature}_enabled".to_sym, true)
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
end
end
it "converts fields from false to ProjectFeature::DISABLED" do
features.each do |feature|
features_except_repository.each do |feature|
project.update_attribute("#{feature}_enabled".to_sym, false)
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
end
end
it "accepts private as ProjectFeature::PRIVATE" do
features.each do |feature|
project.update!("#{feature}_access_level".to_sym => 'private')
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::PRIVATE)
end
end
end
......@@ -1102,6 +1102,12 @@ describe API::Projects do
expect(json_response['wiki_enabled']).to be_present
expect(json_response['jobs_enabled']).to be_present
expect(json_response['snippets_enabled']).to be_present
expect(json_response['snippets_access_level']).to be_present
expect(json_response['repository_access_level']).to be_present
expect(json_response['issues_access_level']).to be_present
expect(json_response['merge_requests_access_level']).to be_present
expect(json_response['wiki_access_level']).to be_present
expect(json_response['builds_access_level']).to be_present
expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['created_at']).to be_present
......@@ -1913,6 +1919,34 @@ describe API::Projects do
end
end
it 'updates builds_access_level' do
project_param = { builds_access_level: 'private' }
put api("/projects/#{project3.id}", user), params: project_param
expect(response).to have_gitlab_http_status(200)
expect(json_response['builds_access_level']).to eq('private')
end
it 'updates build_git_strategy' do
project_param = { build_git_strategy: 'clone' }
put api("/projects/#{project3.id}", user), params: project_param
expect(response).to have_gitlab_http_status(200)
expect(json_response['build_git_strategy']).to eq('clone')
end
it 'rejects to update build_git_strategy when build_git_strategy is invalid' do
project_param = { build_git_strategy: 'invalid' }
put api("/projects/#{project3.id}", user), params: project_param
expect(response).to have_gitlab_http_status(400)
end
it 'updates merge_method' do
project_param = { merge_method: 'ff' }
......@@ -1946,6 +1980,26 @@ describe API::Projects do
'-/system/project/avatar/'\
"#{project3.id}/banana_sample.gif")
end
it 'updates auto_devops_deploy_strategy' do
project_param = { auto_devops_deploy_strategy: 'timed_incremental' }
put api("/projects/#{project3.id}", user), params: project_param
expect(response).to have_gitlab_http_status(200)
expect(json_response['auto_devops_deploy_strategy']).to eq('timed_incremental')
end
it 'updates auto_devops_enabled' do
project_param = { auto_devops_enabled: false }
put api("/projects/#{project3.id}", user), params: project_param
expect(response).to have_gitlab_http_status(200)
expect(json_response['auto_devops_enabled']).to eq(false)
end
end
context 'when authenticated as project maintainer' do
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment