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';
import initUsagePingConsent from './usage_ping_consent';
import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete';
import initSourcegraph from './sourcegraph';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import { initUserTracking } from './tracking';
......@@ -161,10 +160,6 @@ function deferredInitialisation() {
});
loadAwardsHandler();
if (gon.sourcegraph_enabled) {
initSourcegraph();
}
}
document.addEventListener('DOMContentLoaded', () => {
......
import initSourcegraph from '~/sourcegraph';
import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
import initCreateCluster from '~/create_cluster/init_create_cluster';
......@@ -7,4 +8,6 @@ document.addEventListener('DOMContentLoaded', () => {
new Project(); // 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
* code intelligence.
*/
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 scriptPath = new URL('scripts/integration.bundle.js', assetsUrl).href;
window.SOURCEGRAPH_ASSETS_URL = assetsUrl.href;
window.SOURCEGRAPH_URL = sourcegraphUrl;
window.SOURCEGRAPH_INTEGRATION = 'gitlab-integration';
// inject a <script> tag to fetch the main JS bundle from the Sourcegraph instance
const script = document.createElement('script');
script.type = 'application/javascript';
script.src = new URL('scripts/integration.bundle.js', assetsUrl).href;
script.defer = true;
document.head.appendChild(script);
loadScript(scriptPath);
}
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
:preferred_language,
:time_display_relative,
:time_format_in_24h,
:show_whitespace_in_diffs
:show_whitespace_in_diffs,
:sourcegraph_enabled
]
end
end
......
......@@ -4,10 +4,12 @@ class Projects::ApplicationController < ApplicationController
include CookiesHelper
include RoutableActions
include ChecksCollaboration
include SourcegraphGon
skip_before_action :authenticate_user!
before_action :project
before_action :repository
before_action :push_sourcegraph_gon
layout 'project'
helper_method :repository, :can_collaborate_with_project?, :user_access
......
......@@ -261,6 +261,7 @@ module ApplicationSettingsHelper
:signup_enabled,
:sourcegraph_enabled,
:sourcegraph_url,
:sourcegraph_public_only,
:terminal_max_session_time,
:terms,
: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
end
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
transaction(requires_new: true) do
super
......
......@@ -104,6 +104,7 @@ module ApplicationSettingImplementation
signup_enabled: Settings.gitlab['signup_enabled'],
sourcegraph_enabled: false,
sourcegraph_url: nil,
sourcegraph_public_only: true,
terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
......
......@@ -240,6 +240,7 @@ class User < ApplicationRecord
delegate :time_display_relative, :time_display_relative=, 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 :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference
delegate :setup_for_company, :setup_for_company=, to: :user_preference
accepts_nested_attributes_for :user_preference, update_only: true
......
......@@ -24,6 +24,10 @@ class UserPreference < ApplicationRecord
end
end
def sourcegraph_enabled
super && Gitlab::CurrentSettings.sourcegraph_enabled
end
def set_notes_filter(filter_id, issuable)
# No need to update the column if the value is already set.
if filter_id && NOTES_FILTERS.value?(filter_id)
......
- return unless Gitlab::Sourcegraph.feature_available?
- expanded = integration_expanded?('sourcegraph_')
%section.settings.as-sourcegraph.no-animate#js-sourcegraph-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
......@@ -19,7 +21,12 @@
= f.label :sourcegraph_enabled, _('Enable Sourcegraph'), class: 'form-check-label'
.form-group
= 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
= _('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'
- 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 @@
= time_display_label
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
= render 'sourcegraph', f: f
.col-lg-4.profile-settings-sidebar
.col-lg-8
.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
t.string "default_ci_config_path", limit: 255
t.boolean "sourcegraph_enabled", default: false, null: false
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 ["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"
......@@ -3773,6 +3774,7 @@ ActiveRecord::Schema.define(version: 2019_11_12_232338) do
t.boolean "time_format_in_24h"
t.string "projects_sort", limit: 64
t.boolean "show_whitespace_in_diffs", default: true, null: false
t.boolean "sourcegraph_enabled"
t.boolean "setup_for_company"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true
end
......
......@@ -32,9 +32,6 @@ module Gitlab
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
gon.ee = Gitlab.ee?
gon.sourcegraph_enabled = Gitlab::CurrentSettings.sourcegraph_enabled
gon.sourcegraph_url = Gitlab::CurrentSettings.sourcegraph_url
if current_user
gon.current_user_id = current_user.id
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
storages = Gitlab.config.repositories.storages
.merge({ 'custom' => 'tmp/tests/custom_repositories' })
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
Feature.get(:sourcegraph).enable
end
it "updates application settings" do
......@@ -60,6 +61,7 @@ describe API::Settings, 'Settings' do
plantuml_url: 'http://plantuml.example.com',
sourcegraph_enabled: true,
sourcegraph_url: 'https://sourcegraph.com',
sourcegraph_public_only: false,
default_snippet_visibility: 'internal',
restricted_visibility_levels: ['public'],
default_artifacts_expire_in: '2 days',
......@@ -94,6 +96,7 @@ describe API::Settings, 'Settings' do
expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
expect(json_response['sourcegraph_enabled']).to be_truthy
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['restricted_visibility_levels']).to eq(['public'])
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