From db285b625d4c81ab5cf8e95014d96404b1b2a55f Mon Sep 17 00:00:00 2001
From: Guillaume Grossetie <g.grossetie@gmail.com>
Date: Wed, 17 Feb 2021 01:27:06 +0000
Subject: [PATCH] Upgrade to Asciidoctor Kroki 0.3.0

- Add excalidraw as a supported diagram

https://github.com/Mogztter/asciidoctor-kroki/releases/tag/ruby-v0.3.0
---
 Gemfile                                       |  2 +-
 Gemfile.lock                                  |  4 +-
 .../admin/application_settings_controller.rb  |  1 +
 app/helpers/application_settings_helper.rb    | 11 +++
 app/models/application_setting.rb             | 37 ++++++++++
 .../application_setting_implementation.rb     |  1 +
 .../application_setting_kroki_formats.json    | 10 +++
 .../application_settings/_kroki.html.haml     |  8 +++
 .../241744-additional-formats-kroki.yml       |  5 ++
 ...i_formats_to_application_settings_table.rb | 11 +++
 db/schema_migrations/20201120092000           |  1 +
 db/structure.sql                              |  1 +
 lib/gitlab/kroki.rb                           | 18 +++--
 locale/gitlab.pot                             |  3 +
 .../application_settings_controller_spec.rb   |  7 ++
 .../application_settings_helper_spec.rb       | 29 ++++++++
 spec/lib/gitlab/asciidoc_spec.rb              | 67 +++++++++++++++++++
 spec/models/application_setting_spec.rb       | 44 ++++++++++++
 18 files changed, 251 insertions(+), 9 deletions(-)
 create mode 100644 app/validators/json_schemas/application_setting_kroki_formats.json
 create mode 100644 changelogs/unreleased/241744-additional-formats-kroki.yml
 create mode 100644 db/migrate/20201120092000_add_kroki_formats_to_application_settings_table.rb
 create mode 100644 db/schema_migrations/20201120092000

diff --git a/Gemfile b/Gemfile
index 66ca052dada0d..58eea95642746 100644
--- a/Gemfile
+++ b/Gemfile
@@ -156,7 +156,7 @@ gem 'wikicloth', '0.8.1'
 gem 'asciidoctor', '~> 2.0.10'
 gem 'asciidoctor-include-ext', '~> 0.3.1', require: false
 gem 'asciidoctor-plantuml', '~> 0.0.12'
-gem 'asciidoctor-kroki', '~> 0.2.2', require: false
+gem 'asciidoctor-kroki', '~> 0.3.0', require: false
 gem 'rouge', '~> 3.26.0'
 gem 'truncato', '~> 0.7.11'
 gem 'bootstrap_form', '~> 4.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3962fe2707330..bea01cf000d55 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -84,7 +84,7 @@ GEM
     asciidoctor (2.0.12)
     asciidoctor-include-ext (0.3.1)
       asciidoctor (>= 1.5.6, < 3.0.0)
-    asciidoctor-kroki (0.2.2)
+    asciidoctor-kroki (0.3.0)
       asciidoctor (~> 2.0)
     asciidoctor-plantuml (0.0.12)
       asciidoctor (>= 1.5.6, < 3.0.0)
@@ -1291,7 +1291,7 @@ DEPENDENCIES
   asana (~> 0.10.3)
   asciidoctor (~> 2.0.10)
   asciidoctor-include-ext (~> 0.3.1)
-  asciidoctor-kroki (~> 0.2.2)
+  asciidoctor-kroki (~> 0.3.0)
   asciidoctor-plantuml (~> 0.0.12)
   atlassian-jwt (~> 0.2.0)
   attr_encrypted (~> 3.1.0)
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index eb3de936fadf9..7f7d38a09c580 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -238,6 +238,7 @@ def visible_application_setting_attributes
       *::ApplicationSettingsHelper.visible_attributes,
       *::ApplicationSettingsHelper.external_authorization_service_attributes,
       *ApplicationSetting.repository_storages_weighted_attributes,
+      *ApplicationSetting.kroki_formats_attributes.keys.map { |key| "kroki_formats_#{key}".to_sym },
       :lets_encrypt_notification_email,
       :lets_encrypt_terms_of_service_accepted,
       :domain_denylist_file,
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b3b90c79076a6..30ae535b06fd9 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -26,6 +26,16 @@ def enabled_protocol
     end
   end
 
+  def kroki_available_formats
+    ApplicationSetting.kroki_formats_attributes.map do |key, value|
+      {
+        name: "kroki_formats_#{key}",
+        label: value[:label],
+        value: @application_setting.kroki_formats[key] || false
+      }
+    end
+  end
+
   def storage_weights
     ApplicationSetting.repository_storages_weighted_attributes.map do |attribute|
       storage = attribute.to_s.delete_prefix('repository_storages_weighted_')
@@ -259,6 +269,7 @@ def visible_attributes
       :personal_access_token_prefix,
       :kroki_enabled,
       :kroki_url,
+      :kroki_formats,
       :plantuml_enabled,
       :plantuml_url,
       :polling_interval_multiplier,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6d375a19ffb12..33c058dab96b4 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -29,6 +29,21 @@ def self.repository_storages_weighted_attributes
     @repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze
   end
 
+  def self.kroki_formats_attributes
+    {
+      blockdiag: {
+        label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag and RackDiag)'
+      },
+      bpmn: {
+        label: 'BPMN'
+      },
+      excalidraw: {
+        label: 'Excalidraw'
+      }
+    }
+  end
+
+  store_accessor :kroki_formats, *ApplicationSetting.kroki_formats_attributes.keys, prefix: true
   store_accessor :repository_storages_weighted, *Gitlab.config.repositories.storages.keys, prefix: true
 
   # Include here so it can override methods from
@@ -54,6 +69,7 @@ def self.repository_storages_weighted_attributes
 
   default_value_for :id, 1
   default_value_for :repository_storages_weighted, {}
+  default_value_for :kroki_formats, {}
 
   chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
 
@@ -135,6 +151,8 @@ def self.repository_storages_weighted_attributes
 
   validate :validate_kroki_url, if: :kroki_enabled
 
+  validates :kroki_formats, json_schema: { filename: 'application_setting_kroki_formats' }
+
   validates :plantuml_url,
             presence: true,
             if: :plantuml_enabled
@@ -570,6 +588,25 @@ def recaptcha_or_login_protection_enabled
     end
   end
 
+  kroki_formats_attributes.keys.each do |key|
+    define_method :"kroki_formats_#{key}=" do |value|
+      super(::Gitlab::Utils.to_boolean(value))
+    end
+  end
+
+  def kroki_format_supported?(diagram_type)
+    case diagram_type
+    when 'excalidraw'
+      return kroki_formats_excalidraw
+    when 'bpmn'
+      return kroki_formats_bpmn
+    end
+
+    return kroki_formats_blockdiag if ::Gitlab::Kroki::BLOCKDIAG_FORMATS.include?(diagram_type)
+
+    ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.include?(diagram_type)
+  end
+
   private
 
   def parsed_grafana_url
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e5284d15a4940..2911ae6b1c8f3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -176,6 +176,7 @@ def defaults
         container_registry_expiration_policies_worker_capacity: 0,
         kroki_enabled: false,
         kroki_url: nil,
+        kroki_formats: { blockdiag: false, bpmn: false, excalidraw: false },
         rate_limiting_response_text: nil
       }
     end
diff --git a/app/validators/json_schemas/application_setting_kroki_formats.json b/app/validators/json_schemas/application_setting_kroki_formats.json
new file mode 100644
index 0000000000000..460dc74069fa3
--- /dev/null
+++ b/app/validators/json_schemas/application_setting_kroki_formats.json
@@ -0,0 +1,10 @@
+{
+  "description": "Kroki formats",
+  "type": "object",
+  "properties": {
+    "bpmn": { "type": "boolean" },
+    "excalidraw": { "type": "boolean" },
+    "blockdiag": { "type": "boolean" }
+  },
+  "additionalProperties": false
+}
diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml
index 23848fb8b9b02..cd57d4cca6570 100644
--- a/app/views/admin/application_settings/_kroki.html.haml
+++ b/app/views/admin/application_settings/_kroki.html.haml
@@ -21,5 +21,13 @@
           = f.text_field :kroki_url, class: 'form-control gl-form-input', placeholder: 'http://your-kroki-instance:8000'
           .form-text.text-muted
             = (_('When Kroki is enabled, GitLab sends diagrams to an instance of Kroki to display them as images. You can use the free public cloud instance %{kroki_public_url} or you can %{install_link} on your own infrastructure. Once you\'ve installed Kroki, make sure to update the server URL to point to your instance.') % { kroki_public_url: '<code>https://kroki.io</code>', install_link: link_to('install Kroki', 'https://docs.kroki.io/kroki/setup/install/', target: '_blank') }).html_safe
+        .form-group
+          = f.label :kroki_formats, 'Additional diagram formats', class: 'label-bold'
+          .form-text.text-muted
+            = (_('Using additional formats requires starting the companion containers. Make sure that all %{kroki_images} are running.') % { kroki_images: link_to('required containers', 'https://docs.kroki.io/kroki/setup/install/#_images', target: '_blank') }).html_safe
+          - kroki_available_formats.each do |format|
+            .form-check
+              = f.check_box format[:name], class: 'form-check-input'
+              = f.label format[:name], format[:label], class: 'form-check-label'
 
       = f.submit _('Save changes'), class: "btn gl-button btn-success"
diff --git a/changelogs/unreleased/241744-additional-formats-kroki.yml b/changelogs/unreleased/241744-additional-formats-kroki.yml
new file mode 100644
index 0000000000000..f305ee13553bd
--- /dev/null
+++ b/changelogs/unreleased/241744-additional-formats-kroki.yml
@@ -0,0 +1,5 @@
+---
+title: "Enable/disable additional diagram formats on Kroki"
+merge_request: 49304
+author: Guillaume Grossetie
+type: added
diff --git a/db/migrate/20201120092000_add_kroki_formats_to_application_settings_table.rb b/db/migrate/20201120092000_add_kroki_formats_to_application_settings_table.rb
new file mode 100644
index 0000000000000..a059099dbc4f1
--- /dev/null
+++ b/db/migrate/20201120092000_add_kroki_formats_to_application_settings_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddKrokiFormatsToApplicationSettingsTable < ActiveRecord::Migration[6.0]
+  DOWNTIME = false
+
+  def change
+    change_table :application_settings do |t|
+      t.jsonb :kroki_formats, null: false, default: {}
+    end
+  end
+end
diff --git a/db/schema_migrations/20201120092000 b/db/schema_migrations/20201120092000
new file mode 100644
index 0000000000000..eaa6c37cff27e
--- /dev/null
+++ b/db/schema_migrations/20201120092000
@@ -0,0 +1 @@
+c8f837a5fe7a1959af41f19f93b6dd96d8907a476626f124876ee8b10b120b71
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 429f233c8d568..f3feb2573de07 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9398,6 +9398,7 @@ CREATE TABLE application_settings (
     keep_latest_artifact boolean DEFAULT true NOT NULL,
     notes_create_limit integer DEFAULT 300 NOT NULL,
     notes_create_limit_allowlist text[] DEFAULT '{}'::text[] NOT NULL,
+    kroki_formats jsonb DEFAULT '{}'::jsonb NOT NULL,
     CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
     CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
     CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
diff --git a/lib/gitlab/kroki.rb b/lib/gitlab/kroki.rb
index 8c5652fb766e3..38090786836b4 100644
--- a/lib/gitlab/kroki.rb
+++ b/lib/gitlab/kroki.rb
@@ -13,9 +13,7 @@ module Kroki
         packetdiag
         rackdiag
       ].freeze
-    # Diagrams that require a companion container are disabled for now
     DIAGRAMS_FORMATS = ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES
-                           .reject { |diagram_type| diagram_type == 'mermaid' || diagram_type == 'bpmn' || BLOCKDIAG_FORMATS.include?(diagram_type) }
     DIAGRAMS_FORMATS_WO_PLANTUML = DIAGRAMS_FORMATS
                                         .reject { |diagram_type| diagram_type == 'plantuml' }
 
@@ -28,10 +26,18 @@ def self.formats(current_settings)
 
       # If PlantUML is enabled, PlantUML diagrams will be processed by the PlantUML server.
       # In other words, the PlantUML server has precedence over Kroki since both can process PlantUML diagrams.
-      if current_settings.plantuml_enabled
-        DIAGRAMS_FORMATS_WO_PLANTUML
-      else
-        DIAGRAMS_FORMATS
+      diagram_formats = if current_settings.plantuml_enabled
+                          DIAGRAMS_FORMATS_WO_PLANTUML
+                        else
+                          DIAGRAMS_FORMATS
+                        end
+
+      # No additional diagram formats
+      return diagram_formats unless current_settings.kroki_formats.present?
+
+      # Diagrams that require a companion container must be explicitly enabled from the settings
+      diagram_formats.select do |diagram_type|
+        current_settings.kroki_format_supported?(diagram_type)
       end
     end
   end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3847742c1b588..5076242892788 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -32256,6 +32256,9 @@ msgstr ""
 msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}"
 msgstr ""
 
+msgid "Using additional formats requires starting the companion containers. Make sure that all %{kroki_images} are running."
+msgstr ""
+
 msgid "Using required encryption strategy when encrypted field is missing!"
 msgstr ""
 
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index f0b224484c6c2..71abf3191b89f 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -150,6 +150,13 @@
       expect(ApplicationSetting.current.repository_storages_weighted_default).to eq(75)
     end
 
+    it 'updates kroki_formats setting' do
+      put :update, params: { application_setting: { kroki_formats_excalidraw: '1' } }
+
+      expect(response).to redirect_to(general_admin_application_settings_path)
+      expect(ApplicationSetting.current.kroki_formats_excalidraw).to eq(true)
+    end
+
     it "updates default_branch_name setting" do
       put :update, params: { application_setting: { default_branch_name: "example_branch_name" } }
 
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 479e2d7ef9d6f..2cd01451e0d12 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -194,4 +194,33 @@
       it { is_expected.to be false }
     end
   end
+
+  describe '.kroki_available_formats' do
+    let(:application_setting) { build(:application_setting) }
+
+    before do
+      helper.instance_variable_set(:@application_setting, application_setting)
+      stub_application_setting(kroki_formats: { 'blockdiag' => true, 'bpmn' => false, 'excalidraw' => false })
+    end
+
+    it 'returns available formats correctly' do
+      expect(helper.kroki_available_formats).to eq([
+                                             {
+                                               name: 'kroki_formats_blockdiag',
+                                               label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag and RackDiag)',
+                                               value: true
+                                             },
+                                             {
+                                               name: 'kroki_formats_bpmn',
+                                               label: 'BPMN',
+                                               value: false
+                                             },
+                                             {
+                                               name: 'kroki_formats_excalidraw',
+                                               label: 'Excalidraw',
+                                               value: false
+                                             }
+                                           ])
+    end
+  end
 end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 36e4decdeadbe..08510d4652b10 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -510,6 +510,73 @@ module Gitlab
 
           expect(render(input, context)).to include(output.strip)
         end
+
+        it 'does not convert a blockdiag diagram to image' do
+          input = <<~ADOC
+            [blockdiag]
+            ....
+            blockdiag {
+              Kroki -> generates -> "Block diagrams";
+              Kroki -> is -> "very easy!";
+
+              Kroki [color = "greenyellow"];
+              "Block diagrams" [color = "pink"];
+              "very easy!" [color = "orange"];
+            }
+            ....
+          ADOC
+
+          output = <<~HTML
+            <div>
+            <div>
+            <pre>blockdiag {
+              Kroki -&gt; generates -&gt; "Block diagrams";
+              Kroki -&gt; is -&gt; "very easy!";
+
+              Kroki [color = "greenyellow"];
+              "Block diagrams" [color = "pink"];
+              "very easy!" [color = "orange"];
+            }</pre>
+            </div>
+            </div>
+          HTML
+
+          expect(render(input, context)).to include(output.strip)
+        end
+      end
+
+      context 'with Kroki and BlockDiag (additional format) enabled' do
+        before do
+          allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
+          allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
+          allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true)
+        end
+
+        it 'converts a blockdiag diagram to image' do
+          input = <<~ADOC
+            [blockdiag]
+            ....
+            blockdiag {
+              Kroki -> generates -> "Block diagrams";
+              Kroki -> is -> "very easy!";
+
+              Kroki [color = "greenyellow"];
+              "Block diagrams" [color = "pink"];
+              "very easy!" [color = "orange"];
+            }
+            ....
+          ADOC
+
+          output = <<~HTML
+            <div>
+            <div>
+            <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
+            </div>
+            </div>
+          HTML
+
+          expect(render(input, context)).to include(output.strip)
+        end
       end
     end
 
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index efa9125bff994..9a4dd2c799b7f 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -964,6 +964,50 @@ def expect_invalid
     end
   end
 
+  describe 'kroki_format_supported?' do
+    it 'returns true when Excalidraw is enabled' do
+      subject.kroki_formats_excalidraw = true
+      expect(subject.kroki_format_supported?('excalidraw')).to eq(true)
+    end
+
+    it 'returns true when BlockDiag is enabled' do
+      subject.kroki_formats_blockdiag = true
+      # format "blockdiag" aggregates multiple diagram types: actdiag, blockdiag, nwdiag...
+      expect(subject.kroki_format_supported?('actdiag')).to eq(true)
+      expect(subject.kroki_format_supported?('blockdiag')).to eq(true)
+    end
+
+    it 'returns false when BlockDiag is disabled' do
+      subject.kroki_formats_blockdiag = false
+      # format "blockdiag" aggregates multiple diagram types: actdiag, blockdiag, nwdiag...
+      expect(subject.kroki_format_supported?('actdiag')).to eq(false)
+      expect(subject.kroki_format_supported?('blockdiag')).to eq(false)
+    end
+
+    it 'returns false when the diagram type is optional and not enabled' do
+      expect(subject.kroki_format_supported?('bpmn')).to eq(false)
+    end
+
+    it 'returns true when the diagram type is enabled by default' do
+      expect(subject.kroki_format_supported?('vegalite')).to eq(true)
+      expect(subject.kroki_format_supported?('nomnoml')).to eq(true)
+      expect(subject.kroki_format_supported?('unknown-diagram-type')).to eq(false)
+    end
+
+    it 'returns false when the diagram type is unknown' do
+      expect(subject.kroki_format_supported?('unknown-diagram-type')).to eq(false)
+    end
+  end
+
+  describe 'kroki_formats' do
+    it 'returns the value for kroki_formats' do
+      subject.kroki_formats = { blockdiag: true, bpmn: false, excalidraw: true }
+      expect(subject.kroki_formats_blockdiag).to eq(true)
+      expect(subject.kroki_formats_bpmn).to eq(false)
+      expect(subject.kroki_formats_excalidraw).to eq(true)
+    end
+  end
+
   it 'does not allow to set weight for non existing storage' do
     setting.repository_storages_weighted = { invalid_storage: 100 }
 
-- 
GitLab