Commit 9612a1fc authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'gitaly-feature-flag-per-project' into 'master'

Allow setting Gitaly feature flags for individual projects

See merge request gitlab-org/gitlab!53387
parents 603032e1 377215a7
...@@ -124,7 +124,7 @@ module API ...@@ -124,7 +124,7 @@ module API
repository: repository.gitaly_repository.to_h, repository: repository.gitaly_repository.to_h,
address: Gitlab::GitalyClient.address(repository.shard), address: Gitlab::GitalyClient.address(repository.shard),
token: Gitlab::GitalyClient.token(repository.shard), token: Gitlab::GitalyClient.token(repository.shard),
features: Feature::Gitaly.server_feature_flags features: Feature::Gitaly.server_feature_flags(repository.project)
} }
end end
end end
......
...@@ -5,25 +5,25 @@ class Feature ...@@ -5,25 +5,25 @@ class Feature
PREFIX = "gitaly_" PREFIX = "gitaly_"
class << self class << self
def enabled?(feature_flag) def enabled?(feature_flag, project = nil)
return false unless Feature::FlipperFeature.table_exists? return false unless Feature::FlipperFeature.table_exists?
Feature.enabled?("#{PREFIX}#{feature_flag}") Feature.enabled?("#{PREFIX}#{feature_flag}", project)
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
false false
end end
def server_feature_flags def server_feature_flags(project = nil)
# We need to check that both the DB connection and table exists # We need to check that both the DB connection and table exists
return {} unless ::Gitlab::Database.cached_table_exists?(FlipperFeature.table_name) return {} unless ::Gitlab::Database.cached_table_exists?(FlipperFeature.table_name)
Feature.persisted_names Feature.persisted_names
.select { |f| f.start_with?(PREFIX) } .select { |f| f.start_with?(PREFIX) }
.map do |f| .to_h do |f|
flag = f.delete_prefix(PREFIX) flag = f.delete_prefix(PREFIX)
["gitaly-feature-#{flag.tr('_', '-')}", enabled?(flag).to_s] ["gitaly-feature-#{flag.tr('_', '-')}", enabled?(flag, project).to_s]
end.to_h end
end end
end end
end end
......
...@@ -32,7 +32,7 @@ module Gitlab ...@@ -32,7 +32,7 @@ module Gitlab
GitalyServer: { GitalyServer: {
address: Gitlab::GitalyClient.address(repository.storage), address: Gitlab::GitalyClient.address(repository.storage),
token: Gitlab::GitalyClient.token(repository.storage), token: Gitlab::GitalyClient.token(repository.storage),
features: Feature::Gitaly.server_feature_flags features: Feature::Gitaly.server_feature_flags(repository.project)
} }
} }
...@@ -231,7 +231,7 @@ module Gitlab ...@@ -231,7 +231,7 @@ module Gitlab
{ {
address: Gitlab::GitalyClient.address(repository.shard), address: Gitlab::GitalyClient.address(repository.shard),
token: Gitlab::GitalyClient.token(repository.shard), token: Gitlab::GitalyClient.token(repository.shard),
features: Feature::Gitaly.server_feature_flags features: Feature::Gitaly.server_feature_flags(repository.project)
} }
end end
......
...@@ -3,35 +3,78 @@ ...@@ -3,35 +3,78 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Feature::Gitaly do RSpec.describe Feature::Gitaly do
let(:feature_flag) { "mep_mep" } let_it_be(:project) { create(:project) }
let_it_be(:project_2) { create(:project) }
before do
skip_feature_flags_yaml_validation
end
describe ".enabled?" do describe ".enabled?" do
context 'when the gate is closed' do context 'when the flag is set globally' do
before do let(:feature_flag) { 'global_flag' }
stub_feature_flags(gitaly_mep_mep: false)
context 'when the gate is closed' do
before do
stub_feature_flags(gitaly_global_flag: false)
end
it 'returns false' do
expect(described_class.enabled?(feature_flag)).to be(false)
end
end end
it 'returns false' do context 'when the flag defaults to on' do
expect(described_class.enabled?(feature_flag)).to be(false) it 'returns true' do
expect(described_class.enabled?(feature_flag)).to be(true)
end
end end
end end
context 'when the flag defaults to on' do context 'when the flag is enabled for a particular project' do
it 'returns true' do let(:feature_flag) { 'project_flag' }
expect(described_class.enabled?(feature_flag)).to be(true)
before do
stub_feature_flags(gitaly_project_flag: project)
end
it 'returns true for that project' do
expect(described_class.enabled?(feature_flag, project)).to be(true)
end
it 'returns false for any other project' do
expect(described_class.enabled?(feature_flag, project_2)).to be(false)
end
it 'returns false when no project is passed' do
expect(described_class.enabled?(feature_flag)).to be(false)
end end
end end
end end
describe ".server_feature_flags" do describe ".server_feature_flags" do
before do before do
stub_feature_flags(gitaly_mep_mep: true, foo: true) stub_feature_flags(gitaly_global_flag: true, gitaly_project_flag: project, non_gitaly_flag: false)
end end
subject { described_class.server_feature_flags } subject { described_class.server_feature_flags }
it { is_expected.to be_a(Hash) } it 'returns a hash of flags starting with the prefix, with dashes instead of underscores' do
it { is_expected.to eq("gitaly-feature-mep-mep" => "true") } expect(subject).to eq('gitaly-feature-global-flag' => 'true',
'gitaly-feature-project-flag' => 'false')
end
context 'when a project is passed' do
it 'returns the value for the flag on the given project' do
expect(described_class.server_feature_flags(project))
.to eq('gitaly-feature-global-flag' => 'true',
'gitaly-feature-project-flag' => 'true')
expect(described_class.server_feature_flags(project_2))
.to eq('gitaly-feature-global-flag' => 'true',
'gitaly-feature-project-flag' => 'false')
end
end
context 'when table does not exist' do context 'when table does not exist' do
before do before do
......
...@@ -15,9 +15,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -15,9 +15,7 @@ RSpec.describe Gitlab::Workhorse do
end end
before do before do
allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({ stub_feature_flags(gitaly_enforce_requests_limits: true)
'gitaly-feature-foobar' => 'true'
})
end end
describe ".send_git_archive" do describe ".send_git_archive" do
...@@ -43,7 +41,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -43,7 +41,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-archive') expect(command).to eq('git-archive')
expect(params).to eq({ expect(params).to eq({
'GitalyServer' => { 'GitalyServer' => {
features: { 'gitaly-feature-foobar' => 'true' }, features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage) token: Gitlab::GitalyClient.token(project.repository_storage)
}, },
...@@ -73,7 +71,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -73,7 +71,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-archive') expect(command).to eq('git-archive')
expect(params).to eq({ expect(params).to eq({
'GitalyServer' => { 'GitalyServer' => {
features: { 'gitaly-feature-foobar' => 'true' }, features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage) token: Gitlab::GitalyClient.token(project.repository_storage)
}, },
...@@ -124,7 +122,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -124,7 +122,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-format-patch") expect(command).to eq("git-format-patch")
expect(params).to eq({ expect(params).to eq({
'GitalyServer' => { 'GitalyServer' => {
features: { 'gitaly-feature-foobar' => 'true' }, features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage) token: Gitlab::GitalyClient.token(project.repository_storage)
}, },
...@@ -187,7 +185,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -187,7 +185,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-diff") expect(command).to eq("git-diff")
expect(params).to eq({ expect(params).to eq({
'GitalyServer' => { 'GitalyServer' => {
features: { 'gitaly-feature-foobar' => 'true' }, features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage) token: Gitlab::GitalyClient.token(project.repository_storage)
}, },
...@@ -274,7 +272,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -274,7 +272,7 @@ RSpec.describe Gitlab::Workhorse do
let(:gitaly_params) do let(:gitaly_params) do
{ {
GitalyServer: { GitalyServer: {
features: { 'gitaly-feature-foobar' => 'true' }, features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address('default'), address: Gitlab::GitalyClient.address('default'),
token: Gitlab::GitalyClient.token('default') token: Gitlab::GitalyClient.token('default')
} }
...@@ -310,6 +308,35 @@ RSpec.describe Gitlab::Workhorse do ...@@ -310,6 +308,35 @@ RSpec.describe Gitlab::Workhorse do
it { is_expected.to include(ShowAllRefs: true) } it { is_expected.to include(ShowAllRefs: true) }
end end
context 'when a feature flag is set for a single project' do
before do
stub_feature_flags(gitaly_mep_mep: project)
end
it 'sets the flag to true for that project' do
response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action)
expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
'gitaly-feature-mep-mep' => 'true')
end
it 'sets the flag to false for other projects' do
other_project = create(:project, :public, :repository)
response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action)
expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
'gitaly-feature-mep-mep' => 'false')
end
it 'sets the flag to false when there is no project' do
snippet = create(:personal_snippet, :repository)
response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action)
expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
'gitaly-feature-mep-mep' => 'false')
end
end
end end
context "when git_receive_pack action is passed" do context "when git_receive_pack action is passed" do
...@@ -423,7 +450,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -423,7 +450,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-blob') expect(command).to eq('git-blob')
expect(params).to eq({ expect(params).to eq({
'GitalyServer' => { 'GitalyServer' => {
features: { 'gitaly-feature-foobar' => 'true' }, features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage) token: Gitlab::GitalyClient.token(project.repository_storage)
}, },
...@@ -485,7 +512,7 @@ RSpec.describe Gitlab::Workhorse do ...@@ -485,7 +512,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-snapshot') expect(command).to eq('git-snapshot')
expect(params).to eq( expect(params).to eq(
'GitalyServer' => { 'GitalyServer' => {
'features' => { 'gitaly-feature-foobar' => 'true' }, 'features' => { 'gitaly-feature-enforce-requests-limits' => 'true' },
'address' => Gitlab::GitalyClient.address(project.repository_storage), 'address' => Gitlab::GitalyClient.address(project.repository_storage),
'token' => Gitlab::GitalyClient.token(project.repository_storage) 'token' => Gitlab::GitalyClient.token(project.repository_storage)
}, },
......
...@@ -543,25 +543,51 @@ RSpec.describe API::Internal::Base do ...@@ -543,25 +543,51 @@ RSpec.describe API::Internal::Base do
end end
context "git pull" do context "git pull" do
before do context "with a feature flag enabled globally" do
stub_feature_flags(gitaly_mep_mep: true) before do
stub_feature_flags(gitaly_mep_mep: true)
end
it "has the correct payload" do
pull(key, project)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["status"]).to be_truthy
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
expect(user.reload.last_activity_on).to eql(Date.today)
end
end end
it "has the correct payload" do context "with a feature flag enabled for a project" do
pull(key, project) before do
stub_feature_flags(gitaly_mep_mep: project)
end
expect(response).to have_gitlab_http_status(:ok) it "has the flag set to true for that project" do
expect(json_response["status"]).to be_truthy pull(key, project)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gl_project_path"]).to eq(project.full_path) expect(response).to have_gitlab_http_status(:ok)
expect(json_response["gitaly"]).not_to be_nil expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) end
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) it "has the flag set to false for other projects" do
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) other_project = create(:project, :public, :repository)
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
expect(user.reload.last_activity_on).to eql(Date.today) pull(key, other_project)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["gl_repository"]).to eq("project-#{other_project.id}")
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'false')
end
end end
end end
......
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