Commit ddcb7c1d authored by Paul Slaughter's avatar Paul Slaughter

Wrap sourcegraph in feature flag and user opt-in

```---------
DB Migration
```

---------

This commit contains a DB migration adding the following fields:

- `applicaiton_settings::sourcegraph_public_only` this is helpful for
managing the difference between self-hosted and gitlab.com, where on
.com Sourcegraph is not authorized to see private projects, but in a
self-hosted instance, it's based on the authentication token they
preconfigure with their private sourcegraph instance.

- `user_preferences::sourcegraph_enabled` this is used to determine
if the user has opted in for sourcegraph or not.

------------
Feature flag
------------

Example:

```
Feature.enable(:sourcegraph, Project.find_by_full_path('lorem/ipsum'))
```

It is possible to conditionally apply this feature flag, so that the
bundle is only loaded on certain projects. This makes showing the admin
(or user) settings based on this flag difficult since there is no project
or group in scope for these views. For this reason, we've introduced the
`Gitlab::Sourcegraph` module to encapsulate whether a feature is
available (conditionally or globally).

How?

Conditional or global enablement can be tested with:

```
!Feature.get(:sourcegraph).off?
```

https://github.com/jnunemaker/flipper/blob/fa78a0030c7f139aecc3f9c8468baf9fd1498eb9/lib/flipper/feature.rb#L223

----
Also
----

The bundle is only loaded in project routes (potential for further optimization here)
parent 7f4049c3
...@@ -33,7 +33,6 @@ import initBreadcrumbs from './breadcrumb'; ...@@ -33,7 +33,6 @@ import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent'; import initUsagePingConsent from './usage_ping_consent';
import initPerformanceBar from './performance_bar'; import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete'; import initSearchAutocomplete from './search_autocomplete';
import initSourcegraph from './sourcegraph';
import GlFieldErrors from './gl_field_errors'; import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers'; import initUserPopovers from './user_popovers';
import { initUserTracking } from './tracking'; import { initUserTracking } from './tracking';
...@@ -161,10 +160,6 @@ function deferredInitialisation() { ...@@ -161,10 +160,6 @@ function deferredInitialisation() {
}); });
loadAwardsHandler(); loadAwardsHandler();
if (gon.sourcegraph_enabled) {
initSourcegraph();
}
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
......
import initSourcegraph from '~/sourcegraph';
import Project from './project'; import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
import initCreateCluster from '~/create_cluster/init_create_cluster'; import initCreateCluster from '~/create_cluster/init_create_cluster';
...@@ -7,4 +8,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -7,4 +8,6 @@ document.addEventListener('DOMContentLoaded', () => {
new Project(); // eslint-disable-line no-new new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
initSourcegraph();
}); });
function loadScript(path) {
const script = document.createElement('script');
script.type = 'application/javascript';
script.src = path;
script.defer = true;
document.head.appendChild(script);
}
/** /**
* Loads the Sourcegraph integration for support for Sourcegraph extensions and * Loads the Sourcegraph integration for support for Sourcegraph extensions and
* code intelligence. * code intelligence.
*/ */
export default function initSourcegraph() { export default function initSourcegraph() {
const sourcegraphUrl = gon.sourcegraph_url; const { sourcegraph_url: sourcegraphUrl, sourcegraph_enabled: sourcegraphEnabled } = gon;
if (!sourcegraphEnabled || !sourcegraphUrl) {
return;
}
const assetsUrl = new URL('/assets/webpack/sourcegraph/', window.location.href); const assetsUrl = new URL('/assets/webpack/sourcegraph/', window.location.href);
const scriptPath = new URL('scripts/integration.bundle.js', assetsUrl).href;
window.SOURCEGRAPH_ASSETS_URL = assetsUrl.href; window.SOURCEGRAPH_ASSETS_URL = assetsUrl.href;
window.SOURCEGRAPH_URL = sourcegraphUrl; window.SOURCEGRAPH_URL = sourcegraphUrl;
window.SOURCEGRAPH_INTEGRATION = 'gitlab-integration'; window.SOURCEGRAPH_INTEGRATION = 'gitlab-integration';
// inject a <script> tag to fetch the main JS bundle from the Sourcegraph instance
const script = document.createElement('script'); loadScript(scriptPath);
script.type = 'application/javascript';
script.src = new URL('scripts/integration.bundle.js', assetsUrl).href;
script.defer = true;
document.head.appendChild(script);
} }
module SourcegraphGon
extend ActiveSupport::Concern
def push_sourcegraph_gon
return unless enabled?
gon.push({
sourcegraph_enabled: true,
sourcegraph_url: Gitlab::CurrentSettings.sourcegraph_url
})
end
private
def enabled?
current_user&.sourcegraph_enabled && project_enabled?
end
def project_enabled?
return false unless project && Gitlab::Sourcegraph.feature_enabled?(project)
return project.public? if Gitlab::CurrentSettings.sourcegraph_public_only
true
end
def project
@target_project || @project
end
end
...@@ -47,7 +47,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -47,7 +47,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:preferred_language, :preferred_language,
:time_display_relative, :time_display_relative,
:time_format_in_24h, :time_format_in_24h,
:show_whitespace_in_diffs :show_whitespace_in_diffs,
:sourcegraph_enabled
] ]
end end
end end
......
...@@ -4,10 +4,12 @@ class Projects::ApplicationController < ApplicationController ...@@ -4,10 +4,12 @@ class Projects::ApplicationController < ApplicationController
include CookiesHelper include CookiesHelper
include RoutableActions include RoutableActions
include ChecksCollaboration include ChecksCollaboration
include SourcegraphGon
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :project before_action :project
before_action :repository before_action :repository
before_action :push_sourcegraph_gon
layout 'project' layout 'project'
helper_method :repository, :can_collaborate_with_project?, :user_access helper_method :repository, :can_collaborate_with_project?, :user_access
......
...@@ -261,6 +261,7 @@ module ApplicationSettingsHelper ...@@ -261,6 +261,7 @@ module ApplicationSettingsHelper
:signup_enabled, :signup_enabled,
:sourcegraph_enabled, :sourcegraph_enabled,
:sourcegraph_url, :sourcegraph_url,
:sourcegraph_public_only,
:terminal_max_session_time, :terminal_max_session_time,
:terms, :terms,
:throttle_authenticated_api_enabled, :throttle_authenticated_api_enabled,
......
# frozen_string_literal: true
module SourcegraphHelper
def sourcegraph_help_message
return unless Gitlab::CurrentSettings.sourcegraph_enabled
if Gitlab::Sourcegraph.feature_conditional?
_("This feature is experimental and has been limited to only certain projects.")
elsif Gitlab::CurrentSettings.sourcegraph_public_only
_("This feature is experimental and also limited to only public projects.")
else
_("This feature is experimental.")
end
end
end
...@@ -347,6 +347,10 @@ class ApplicationSetting < ApplicationRecord ...@@ -347,6 +347,10 @@ class ApplicationSetting < ApplicationRecord
end end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') } after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def sourcegraph_enabled
super && Gitlab::Sourcegraph.feature_available?
end
def self.create_from_defaults def self.create_from_defaults
transaction(requires_new: true) do transaction(requires_new: true) do
super super
......
...@@ -104,6 +104,7 @@ module ApplicationSettingImplementation ...@@ -104,6 +104,7 @@ module ApplicationSettingImplementation
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
sourcegraph_enabled: false, sourcegraph_enabled: false,
sourcegraph_url: nil, sourcegraph_url: nil,
sourcegraph_public_only: true,
terminal_max_session_time: 0, terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false, throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600, throttle_authenticated_api_period_in_seconds: 3600,
......
...@@ -240,6 +240,7 @@ class User < ApplicationRecord ...@@ -240,6 +240,7 @@ class User < ApplicationRecord
delegate :time_display_relative, :time_display_relative=, to: :user_preference delegate :time_display_relative, :time_display_relative=, to: :user_preference
delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference
delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference
delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference
delegate :setup_for_company, :setup_for_company=, to: :user_preference delegate :setup_for_company, :setup_for_company=, to: :user_preference
accepts_nested_attributes_for :user_preference, update_only: true accepts_nested_attributes_for :user_preference, update_only: true
......
...@@ -24,6 +24,10 @@ class UserPreference < ApplicationRecord ...@@ -24,6 +24,10 @@ class UserPreference < ApplicationRecord
end end
end end
def sourcegraph_enabled
super && Gitlab::CurrentSettings.sourcegraph_enabled
end
def set_notes_filter(filter_id, issuable) def set_notes_filter(filter_id, issuable)
# No need to update the column if the value is already set. # No need to update the column if the value is already set.
if filter_id && NOTES_FILTERS.value?(filter_id) if filter_id && NOTES_FILTERS.value?(filter_id)
......
- return unless Gitlab::Sourcegraph.feature_available?
- expanded = integration_expanded?('sourcegraph_') - expanded = integration_expanded?('sourcegraph_')
%section.settings.as-sourcegraph.no-animate#js-sourcegraph-settings{ class: ('expanded' if expanded) } %section.settings.as-sourcegraph.no-animate#js-sourcegraph-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
...@@ -19,7 +21,12 @@ ...@@ -19,7 +21,12 @@
= f.label :sourcegraph_enabled, _('Enable Sourcegraph'), class: 'form-check-label' = f.label :sourcegraph_enabled, _('Enable Sourcegraph'), class: 'form-check-label'
.form-group .form-group
= f.label :sourcegraph_url, _('Sourcegraph URL'), class: 'label-bold' = f.label :sourcegraph_url, _('Sourcegraph URL'), class: 'label-bold'
= f.text_field :sourcegraph_url, class: 'form-control', placeholder: 'https://example.sourcegraph.com' = f.text_field :sourcegraph_url, class: 'form-control', placeholder: 'e.g. https://sourcegraph.example.com'
.form-text.text-muted
= _('Add %{link} code intelligence to your GitLab instance code views and merge requests.').html_safe % { link: link_to('Sourcegraph', 'https://sourcegraph.com/', target: '_blank') }
.form-group
= f.label :sourcegraph_public_only, _('Scope'), class: 'label-bold'
= f.select :sourcegraph_public_only, [[_('All projects'), false], [_('Public projects only'), true]], {}, class: 'form-control'
.form-text.text-muted .form-text.text-muted
= _('Add %{link} code intelligence to your GitLab instance code views and pull requests.').html_safe % { link: link_to('Sourcegraph', 'https://sourcegraph.com/', target: '_blank') } = _('Configure which projects should allow requests to Sourcegraph.')
= f.submit _('Save changes'), class: 'btn btn-success' = f.submit _('Save changes'), class: 'btn btn-success'
- return unless Gitlab::CurrentSettings.sourcegraph_enabled
- sourcegraph_url = Gitlab::CurrentSettings.sourcegraph_url
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= _('Integrations')
%p
= _('Customize integrations with third party services.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/third-party.md', anchor: 'localization'), target: '_blank'
.col-lg-8
%label.label-bold
= s_('Preferences|Sourcegraph')
= link_to icon('question-circle'), help_page_path('user/profile/sourcegraph.md'), target: '_blank'
.form-group.form-check
= f.check_box :sourcegraph_enabled, class: 'form-check-input'
= f.label :sourcegraph_enabled, class: 'form-check-label' do
- link_start = '<a href="%{url}">'.html_safe % { url: sourcegraph_url }
- link_end = '</a>'.html_safe
= s_('Preferences|Enable integrated code intelligence on code views, %{link_start}powered by Sourcegraph%{link_end}.').html_safe % { link_start: link_start, link_end: link_end }
.form-text.text-muted
= sourcegraph_help_message
...@@ -111,6 +111,9 @@ ...@@ -111,6 +111,9 @@
= time_display_label = time_display_label
.form-text.text-muted .form-text.text-muted
= s_('Preferences|For example: 30 mins ago.') = s_('Preferences|For example: 30 mins ago.')
= render 'sourcegraph', f: f
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
.col-lg-8 .col-lg-8
.form-group .form-group
......
# frozen_string_literal: true
class AddSourcegraphAdminAndUserPreferences < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:application_settings, :sourcegraph_public_only, :boolean, default: true)
add_column(:user_preferences, :sourcegraph_enabled, :boolean)
end
def down
remove_column(:application_settings, :sourcegraph_public_only)
remove_column(:user_preferences, :sourcegraph_enabled)
end
end
...@@ -354,6 +354,7 @@ ActiveRecord::Schema.define(version: 2019_11_12_232338) do ...@@ -354,6 +354,7 @@ ActiveRecord::Schema.define(version: 2019_11_12_232338) do
t.string "default_ci_config_path", limit: 255 t.string "default_ci_config_path", limit: 255
t.boolean "sourcegraph_enabled", default: false, null: false t.boolean "sourcegraph_enabled", default: false, null: false
t.string "sourcegraph_url", limit: 255 t.string "sourcegraph_url", limit: 255
t.boolean "sourcegraph_public_only", default: true, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
...@@ -3773,6 +3774,7 @@ ActiveRecord::Schema.define(version: 2019_11_12_232338) do ...@@ -3773,6 +3774,7 @@ ActiveRecord::Schema.define(version: 2019_11_12_232338) do
t.boolean "time_format_in_24h" t.boolean "time_format_in_24h"
t.string "projects_sort", limit: 64 t.string "projects_sort", limit: 64
t.boolean "show_whitespace_in_diffs", default: true, null: false t.boolean "show_whitespace_in_diffs", default: true, null: false
t.boolean "sourcegraph_enabled"
t.boolean "setup_for_company" t.boolean "setup_for_company"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true
end end
......
...@@ -32,9 +32,6 @@ module Gitlab ...@@ -32,9 +32,6 @@ module Gitlab
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
gon.ee = Gitlab.ee? gon.ee = Gitlab.ee?
gon.sourcegraph_enabled = Gitlab::CurrentSettings.sourcegraph_enabled
gon.sourcegraph_url = Gitlab::CurrentSettings.sourcegraph_url
if current_user if current_user
gon.current_user_id = current_user.id gon.current_user_id = current_user.id
gon.current_username = current_user.username gon.current_username = current_user.username
......
# frozen_string_literal: true
module Gitlab
class Sourcegraph
class << self
def feature_conditional?
feature.conditional?
end
def feature_available?
# The sourcegraph_bundle feature could be conditionally applied, so check if `!off?`
!feature.off?
end
def feature_enabled?(thing = true)
feature.enabled?(thing)
end
private
def feature
Feature.get(:sourcegraph)
end
end
end
end
...@@ -46,6 +46,7 @@ describe API::Settings, 'Settings' do ...@@ -46,6 +46,7 @@ describe API::Settings, 'Settings' do
storages = Gitlab.config.repositories.storages storages = Gitlab.config.repositories.storages
.merge({ 'custom' => 'tmp/tests/custom_repositories' }) .merge({ 'custom' => 'tmp/tests/custom_repositories' })
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
Feature.get(:sourcegraph).enable
end end
it "updates application settings" do it "updates application settings" do
...@@ -60,6 +61,7 @@ describe API::Settings, 'Settings' do ...@@ -60,6 +61,7 @@ describe API::Settings, 'Settings' do
plantuml_url: 'http://plantuml.example.com', plantuml_url: 'http://plantuml.example.com',
sourcegraph_enabled: true, sourcegraph_enabled: true,
sourcegraph_url: 'https://sourcegraph.com', sourcegraph_url: 'https://sourcegraph.com',
sourcegraph_public_only: false,
default_snippet_visibility: 'internal', default_snippet_visibility: 'internal',
restricted_visibility_levels: ['public'], restricted_visibility_levels: ['public'],
default_artifacts_expire_in: '2 days', default_artifacts_expire_in: '2 days',
...@@ -94,6 +96,7 @@ describe API::Settings, 'Settings' do ...@@ -94,6 +96,7 @@ describe API::Settings, 'Settings' do
expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
expect(json_response['sourcegraph_enabled']).to be_truthy expect(json_response['sourcegraph_enabled']).to be_truthy
expect(json_response['sourcegraph_url']).to eq('https://sourcegraph.com') expect(json_response['sourcegraph_url']).to eq('https://sourcegraph.com')
expect(json_response['sourcegraph_public_only']).to eq(false)
expect(json_response['default_snippet_visibility']).to eq('internal') expect(json_response['default_snippet_visibility']).to eq('internal')
expect(json_response['restricted_visibility_levels']).to eq(['public']) expect(json_response['restricted_visibility_levels']).to eq(['public'])
expect(json_response['default_artifacts_expire_in']).to eq('2 days') expect(json_response['default_artifacts_expire_in']).to eq('2 days')
......
# frozen_string_literal: true
require 'spec_helper'
describe 'profiles/preferences/show' do
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
before do
assign(:user, user)
allow(controller).to receive(:current_user).and_return(user)
end
context 'sourcegraph' do
def have_sourcegraph_field(*args)
have_field('user_sourcegraph_enabled', *args)
end
def have_integrations_section
have_css('.profile-settings-sidebar', { text: 'Integrations' })
end
before do
# Can't use stub_feature_flags because we use Feature.get to check if conditinally applied
Feature.get(:sourcegraph).enable sourcegraph_feature
Gitlab::CurrentSettings.sourcegraph_enabled = sourcegraph_enabled
end
context 'when not fully enabled' do
where(:feature, :admin_enabled) do
false | false
false | true
true | false
end
with_them do
let(:sourcegraph_feature) { feature }
let(:sourcegraph_enabled) { admin_enabled }
before do
render
end
it 'does not display sourcegraph field' do
expect(rendered).not_to have_sourcegraph_field
end
it 'does not display integrations settings' do
expect(rendered).not_to have_integrations_section
end
end
end
context 'when fully enabled' do
let(:sourcegraph_feature) { true }
let(:sourcegraph_enabled) { true }
before do
render
end
it 'displays the sourcegraph field' do
expect(rendered).to have_sourcegraph_field
end
it 'displays the integrations section' do
expect(rendered).to have_integrations_section
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