Commit 274c3abb authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '220934-confluence-wiki' into 'master'

Add a new Confluence integration service

See merge request gitlab-org/gitlab!36262
parents e9eaa7b1 7273d667
......@@ -22,6 +22,7 @@ module ServiceParams
:comment_on_event_enabled,
:comment_detail,
:confidential_issues_events,
:confluence_url,
:default_irc_uri,
:device,
:disable_diffs,
......
......@@ -400,7 +400,7 @@ module ProjectsHelper
nav_tabs = [:home]
unless project.empty_repo?
nav_tabs << [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project)
nav_tabs += [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project)
nav_tabs << :releases if can?(current_user, :read_release, project)
end
......@@ -421,30 +421,30 @@ module ProjectsHelper
nav_tabs << :operations
end
if can?(current_user, :read_cycle_analytics, project)
nav_tabs << :cycle_analytics
end
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
nav_tabs << external_nav_tabs(project)
apply_external_nav_tabs(nav_tabs, project)
nav_tabs.flatten
nav_tabs
end
def external_nav_tabs(project)
[].tap do |tabs|
tabs << :external_issue_tracker if project.external_issue_tracker
tabs << :external_wiki if project.external_wiki
def apply_external_nav_tabs(nav_tabs, project)
nav_tabs << :external_issue_tracker if project.external_issue_tracker
nav_tabs << :external_wiki if project.external_wiki
if project.has_confluence?
nav_tabs.delete(:wiki)
nav_tabs << :confluence
end
end
def tab_ability_map
{
cycle_analytics: :read_cycle_analytics,
environments: :read_environment,
metrics_dashboards: :metrics_dashboard,
milestones: :read_milestone,
......
......@@ -169,6 +169,7 @@ class Project < ApplicationRecord
has_one :custom_issue_tracker_service
has_one :bugzilla_service
has_one :gitlab_issue_tracker_service, inverse_of: :project
has_one :confluence_service
has_one :external_wiki_service
has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service
......@@ -1286,6 +1287,11 @@ class Project < ApplicationRecord
update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end
def has_confluence?
ConfluenceService.feature_enabled?(self) && # rubocop:disable CodeReuse/ServiceClass
project_setting.has_confluence?
end
def find_or_initialize_services
available_services_names = Service.available_services_names - disabled_services
......@@ -1295,7 +1301,11 @@ class Project < ApplicationRecord
end
def disabled_services
[]
strong_memoize(:disabled_services) do
[].tap do |disabled_services|
disabled_services.push(ConfluenceService.to_param) unless ConfluenceService.feature_enabled?(self) # rubocop:disable CodeReuse/ServiceClass
end
end
end
def find_or_initialize_service(name)
......
# frozen_string_literal: true
class ConfluenceService < Service
include ActionView::Helpers::UrlHelper
VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze
VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze
VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze
FEATURE_FLAG = :confluence_integration
prop_accessor :confluence_url
validates :confluence_url, presence: true, if: :activated?
validate :validate_confluence_url_is_cloud, if: :activated?
after_commit :cache_project_has_confluence
def self.feature_enabled?(actor)
::Feature.enabled?(FEATURE_FLAG, actor)
end
def self.to_param
'confluence'
end
def self.supported_events
%w()
end
def title
s_('ConfluenceService|Confluence Workspace')
end
def description
s_('ConfluenceService|Connect a Confluence Cloud Workspace to your GitLab project')
end
def detailed_description
return unless project.wiki_enabled?
if activated?
wiki_url = project.wiki.web_url
s_(
'ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration' %
{ wiki_link: link_to(wiki_url, wiki_url) }
).html_safe
else
s_('ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration').html_safe
end
end
def fields
[
{
type: 'text',
name: 'confluence_url',
title: 'Confluence Cloud Workspace URL',
placeholder: s_('ConfluenceService|The URL of the Confluence Workspace'),
required: true
}
]
end
def can_test?
false
end
private
def validate_confluence_url_is_cloud
unless confluence_uri_valid?
errors.add(:confluence_url, 'URL must be to a Confluence Cloud Workspace hosted on atlassian.net')
end
end
def confluence_uri_valid?
return false unless confluence_url
uri = URI.parse(confluence_url)
(uri.scheme&.match(VALID_SCHEME_MATCH) &&
uri.host&.match(VALID_HOST_MATCH) &&
uri.path&.match(VALID_PATH_MATCH)).present?
rescue URI::InvalidURIError
false
end
def cache_project_has_confluence
return unless project && !project.destroyed?
project.project_setting.save! unless project.project_setting.persisted?
project.project_setting.update_column(:has_confluence, active?)
end
end
......@@ -12,7 +12,7 @@ class Service < ApplicationRecord
ignore_columns %i[title description], remove_with: '13.4', remove_after: '2020-09-22'
SERVICE_NAMES = %w[
alerts asana assembla bamboo bugzilla buildkite campfire custom_issue_tracker discord
alerts asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
drone_ci emails_on_push external_wiki flowdock hangouts_chat hipchat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
......
......@@ -293,6 +293,20 @@
= render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
- if project_nav_tab?(:confluence)
- confluence_url = @project.confluence_service.confluence_url
= nav_link do
= link_to confluence_url, class: 'shortcuts-confluence' do
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
= _('Confluence')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: 'fly-out-top-item' } ) do
= link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
%strong.fly-out-top-item-name
= _('Confluence')
- if project_nav_tab? :wiki
- wiki_url = wiki_path(@project.wiki)
= nav_link(controller: :wikis) do
......
......@@ -12408,6 +12408,7 @@ enum ServiceType {
BUGZILLA_SERVICE
BUILDKITE_SERVICE
CAMPFIRE_SERVICE
CONFLUENCE_SERVICE
CUSTOM_ISSUE_TRACKER_SERVICE
DISCORD_SERVICE
DRONE_CI_SERVICE
......
......@@ -36439,6 +36439,12 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CONFLUENCE_SERVICE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CUSTOM_ISSUE_TRACKER_SERVICE",
"description": null,
......@@ -493,6 +493,73 @@ Get Emails on push service settings for a project.
GET /projects/:id/services/emails-on-push
```
## Confluence service
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220934) in GitLab 13.2.
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled on GitLab.com.
> - It's able to be enabled or disabled per-project
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to
[enable it](#enable-or-disable-the-confluence-service-core-only). **(CORE ONLY)**
Replaces the link to the internal wiki with a link to a Confluence Cloud Workspace.
### Create/Edit Confluence service
Set Confluence service for a project.
```plaintext
PUT /projects/:id/services/confluence
```
Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `confluence_url` | string | true | The URL of the Confluence Cloud Workspace hosted on atlassian.net. |
### Delete Confluence service
Delete Confluence service for a project.
```plaintext
DELETE /projects/:id/services/confluence
```
### Get Confluence service settings
Get Confluence service settings for a project.
```plaintext
GET /projects/:id/services/confluence
```
### Enable or disable the Confluence service **(CORE ONLY)**
The Confluence service is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can enable it for your instance. The Confluence service can be enabled or disabled per-project
To enable it:
```ruby
# Instance-wide
Feature.enable(:confluence_integration)
# or by project
Feature.enable(:confluence_integration, Project.find(<project id>))
```
To disable it:
```ruby
# Instance-wide
Feature.disable(:confluence_integration)
# or by project
Feature.disable(:confluence_integration, Project.find(<project id>))
```
## External Wiki
Replaces the link to the internal wiki with a link to an external wiki.
......
......@@ -28,6 +28,7 @@ Click on the service links to see further configuration instructions and details
| Buildkite | Continuous integration and deployments | Yes |
| [Bugzilla](bugzilla.md) | Bugzilla issue tracker | No |
| Campfire | Simple web-based real-time group chat | No |
| Confluence | Replaces the link to the internal wiki with a link to a Confluence Cloud Workspace. Service is behind a feature flag, disabled by default ([see details](../../../api/services.md#enable-or-disable-the-confluence-service-core-only)). | No |
| Custom Issue Tracker | Custom issue tracker | No |
| [Discord Notifications](discord_notifications.md) | Receive event notifications in Discord | No |
| Drone CI | Continuous Integration platform built on Docker, written in Go | Yes |
......
......@@ -518,7 +518,7 @@ module EE
override :disabled_services
def disabled_services
strong_memoize(:disabled_services) do
[].tap do |services|
super.tap do |services|
services.push('jenkins') unless feature_available?(:jenkins_integration)
services.push('github') unless feature_available?(:github_project_service_integration)
::Gitlab::CurrentSettings.slack_app_enabled ? services.push('slack_slash_commands') : services.push('gitlab_slack_application')
......
......@@ -288,6 +288,14 @@ module API
desc: 'Campfire room'
}
],
'confluence' => [
{
required: true,
name: :confluence_url,
type: String,
desc: 'The URL of the Confluence Cloud Workspace hosted on atlassian.net'
}
],
'custom-issue-tracker' => [
{
required: true,
......@@ -757,6 +765,7 @@ module API
::BambooService,
::BugzillaService,
::BuildkiteService,
::ConfluenceService,
::CampfireService,
::CustomIssueTrackerService,
::DiscordService,
......
......@@ -6199,6 +6199,24 @@ msgstr ""
msgid "Confirmation required"
msgstr ""
msgid "Confluence"
msgstr ""
msgid "ConfluenceService|Confluence Workspace"
msgstr ""
msgid "ConfluenceService|Connect a Confluence Cloud Workspace to your GitLab project"
msgstr ""
msgid "ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration"
msgstr ""
msgid "ConfluenceService|The URL of the Confluence Workspace"
msgstr ""
msgid "ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration"
msgstr ""
msgid "Congratulations! You have enabled Two-factor Authentication!"
msgstr ""
......
......@@ -89,6 +89,12 @@ FactoryBot.define do
end
end
factory :confluence_service do
project
active { true }
confluence_url { 'https://example.atlassian.net/wiki' }
end
factory :bugzilla_service do
project
active { true }
......
......@@ -444,8 +444,8 @@ RSpec.describe ProjectsHelper do
end
describe '#get_project_nav_tabs' do
let_it_be(:user) { create(:user) }
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
allow(helper).to receive(:can?) { true }
......@@ -501,6 +501,20 @@ RSpec.describe ProjectsHelper do
is_expected.not_to include(:external_wiki)
end
end
context 'when project has confluence enabled' do
before do
allow(project).to receive(:has_confluence?).and_return(true)
end
it { is_expected.to include(:confluence) }
it { is_expected.not_to include(:wiki) }
end
context 'when project does not have confluence enabled' do
it { is_expected.not_to include(:confluence) }
it { is_expected.to include(:wiki) }
end
end
describe '#can_view_operations_tab?' do
......
......@@ -322,6 +322,7 @@ project:
- last_event
- services
- campfire_service
- confluence_service
- discord_service
- drone_ci_service
- emails_on_push_service
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ConfluenceService do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'Validations' do
before do
subject.active = active
end
context 'when service is active' do
let(:active) { true }
it { is_expected.not_to allow_value('https://example.com').for(:confluence_url) }
it { is_expected.not_to allow_value('example.com').for(:confluence_url) }
it { is_expected.not_to allow_value('foo').for(:confluence_url) }
it { is_expected.not_to allow_value('ftp://example.atlassian.net/wiki').for(:confluence_url) }
it { is_expected.not_to allow_value('https://example.atlassian.net').for(:confluence_url) }
it { is_expected.not_to allow_value('https://.atlassian.net/wiki').for(:confluence_url) }
it { is_expected.not_to allow_value('https://example.atlassian.net/wikifoo').for(:confluence_url) }
it { is_expected.not_to allow_value('').for(:confluence_url) }
it { is_expected.not_to allow_value(nil).for(:confluence_url) }
it { is_expected.not_to allow_value('😊').for(:confluence_url) }
it { is_expected.to allow_value('https://example.atlassian.net/wiki').for(:confluence_url) }
it { is_expected.to allow_value('http://example.atlassian.net/wiki').for(:confluence_url) }
it { is_expected.to allow_value('https://example.atlassian.net/wiki/').for(:confluence_url) }
it { is_expected.to allow_value('http://example.atlassian.net/wiki/').for(:confluence_url) }
it { is_expected.to allow_value('https://example.atlassian.net/wiki/foo').for(:confluence_url) }
it { is_expected.to validate_presence_of(:confluence_url) }
end
context 'when service is inactive' do
let(:active) { false }
it { is_expected.not_to validate_presence_of(:confluence_url) }
it { is_expected.to allow_value('foo').for(:confluence_url) }
end
end
describe '#detailed_description' do
it 'can correctly return a link to the project wiki when active' do
project = create(:project)
subject.project = project
subject.active = true
expect(subject.detailed_description).to include(Gitlab::Routing.url_helpers.project_wikis_url(project))
end
context 'when the project wiki is not enabled' do
it 'returns nil when both active or inactive', :aggregate_failures do
project = create(:project, :wiki_disabled)
subject.project = project
[true, false].each do |active|
subject.active = active
expect(subject.detailed_description).to be_nil
end
end
end
end
describe 'Caching has_confluence on project_settings' do
let(:project) { create(:project) }
subject { project.project_setting.has_confluence? }
it 'sets the property to true when service is active' do
create(:confluence_service, project: project, active: true)
is_expected.to be(true)
end
it 'sets the property to false when service is not active' do
create(:confluence_service, project: project, active: false)
is_expected.to be(false)
end
it 'creates a project_setting record if one was not already created' do
expect { create(:confluence_service) }.to change { ProjectSetting.count }.by(1)
end
end
end
......@@ -63,6 +63,7 @@ RSpec.describe Project do
it { is_expected.to have_one(:bugzilla_service) }
it { is_expected.to have_one(:gitlab_issue_tracker_service) }
it { is_expected.to have_one(:external_wiki_service) }
it { is_expected.to have_one(:confluence_service) }
it { is_expected.to have_one(:project_feature) }
it { is_expected.to have_one(:project_repository) }
it { is_expected.to have_one(:container_expiration_policy) }
......@@ -1041,6 +1042,32 @@ RSpec.describe Project do
end
end
describe '#has_confluence?' do
let_it_be(:project) { build_stubbed(:project) }
it 'returns false when project_setting.has_confluence property is false' do
project.project_setting.has_confluence = false
expect(project.has_confluence?).to be(false)
end
context 'when project_setting.has_confluence property is true' do
before do
project.project_setting.has_confluence = true
end
it 'returns true' do
expect(project.has_confluence?).to be(true)
end
it 'returns false when confluence integration feature flag is disabled' do
stub_feature_flags(ConfluenceService::FEATURE_FLAG => false)
expect(project.has_confluence?).to be(false)
end
end
end
describe '#external_wiki' do
let(:project) { create(:project) }
......@@ -5385,6 +5412,20 @@ RSpec.describe Project do
expect(services.count).to eq(2)
expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
end
describe 'interaction with the confluence integration feature flag' do
it 'contains a ConfluenceService when feature flag is enabled' do
stub_feature_flags(ConfluenceService::FEATURE_FLAG => true)
expect(subject.find_or_initialize_services).to include(ConfluenceService)
end
it 'does not contain a ConfluenceService when the confluence integration feature flag is disabled' do
stub_feature_flags(ConfluenceService::FEATURE_FLAG => false)
expect(subject.find_or_initialize_services).not_to include(ConfluenceService)
end
end
end
describe '#find_or_initialize_service' do
......
......@@ -12,6 +12,8 @@ Service.available_services_names.each do |service|
service_attrs_list.inject({}) do |hash, k|
if k =~ /^(token*|.*_token|.*_key)/
hash.merge!(k => 'secrettoken')
elsif service == 'confluence' && k == :confluence_url
hash.merge!(k => 'https://example.atlassian.net/wiki')
elsif k =~ /^(.*_url|url|webhook)/
hash.merge!(k => "http://example.com")
elsif service_klass.method_defined?("#{k}?")
......
......@@ -76,7 +76,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'does not show the wiki tab' do
render
expect(rendered).not_to have_link('Wiki', href: wiki_path(project.wiki))
expect(rendered).not_to have_link('Wiki')
end
end
end
......@@ -109,6 +109,38 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
describe 'confluence tab' do
let!(:service) { create(:confluence_service, project: project, active: active) }
before do
render
end
context 'when the Confluence integration is active' do
let(:active) { true }
it 'shows the Confluence tab' do
expect(rendered).to have_link('Confluence', href: service.confluence_url)
end
it 'does not show the GitLab wiki tab' do
expect(rendered).not_to have_link('Wiki')
end
end
context 'when it is disabled' do
let(:active) { false }
it 'does not show the Confluence tab' do
expect(rendered).not_to have_link('Confluence')
end
it 'shows the GitLab wiki tab' do
expect(rendered).to have_link('Wiki', href: wiki_path(project.wiki))
end
end
end
describe 'ci/cd settings tab' do
before do
project.update!(archived: project_archived)
......
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