[DRE-commits] [redmine] 05/05: Add multi-tenancy support

Antonio Terceiro terceiro at moszumanska.debian.org
Fri Jan 22 13:01:26 UTC 2016


This is an automated email from the git hooks/post-receive script.

terceiro pushed a commit to branch patch-queue/master
in repository redmine.

commit 2c035ff89dfcba456adc9e5d788ecc375d97d66c
Author: Antonio Terceiro <terceiro at debian.org>
Date:   Mon Sep 21 12:31:42 2015 -0300

    Add multi-tenancy support
    
    This is an improved version of the combination of a few patches that
    were carried in the Redmine package for Debian GNU/Linux for a few
    years.
    
    Documentation is provided as a man page produced by
    `./bin/redmine-instances help`
    
    Signed-off-by: Antonio Terceiro <terceiro at debian.org>
    Signed-off-by: Jérémy Lal <kapouer at melix.org>
    Signed-off-by: Ondřej Surý <ondrej at sury.org>
    
    Gbp-Pq: Name 0004-Add-multi-tenancy-support.patch
---
 .gitignore                                         |   1 +
 Gemfile                                            |   9 +-
 app/models/attachment.rb                           |   4 +-
 bin/redmine-instances                              | 280 +++++++++++++++++++++
 config/application.rb                              |   1 +
 config/multitenancy_environment.rb                 |  42 ++++
 .../lib/open_id_authentication.rb                  |   2 +-
 lib/redmine/configuration.rb                       |   4 +-
 lib/redmine/export/pdf.rb                          |   2 +-
 lib/redmine/multi_tenancy.rb                       |  43 ++++
 lib/redmine/scm/adapters/abstract_adapter.rb       |   2 +-
 lib/tasks/initializers.rake                        |   2 +-
 12 files changed, 383 insertions(+), 9 deletions(-)

diff --git a/.gitignore b/.gitignore
index 173b030..d9e39ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@
 /db/*.sqlite3
 /db/schema.rb
 /files/*
+/instances
 /lib/redmine/scm/adapters/mercurial/redminehelper.pyc
 /lib/redmine/scm/adapters/mercurial/redminehelper.pyo
 /log/*.log*
diff --git a/Gemfile b/Gemfile
index 24d8257..cd7936b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -52,7 +52,14 @@ end
 # configuration file
 require 'erb'
 require 'yaml'
-database_file = File.join(File.dirname(__FILE__), "config/database.yml")
+
+# XXX this duplicates logic in lib/redmine/multi_tenancy.rb, but we can't load
+# XXX that at this point. X_DEBIAN_SITEID is kept for compatibility with
+# existing XXX Debian installations.
+instance = ENV['X_DEBIAN_SITEID'] || ENV['REDMINE_INSTANCE']
+root = instance && File.join(File.dirname(__FILE__), 'instances', instance) || File.dirname(__FILE__)
+database_file = File.join(root, "config/database.yml")
+
 if File.exist?(database_file)
   database_config = YAML::load(ERB.new(IO.read(database_file)).result)
   adapters = database_config.values.map {|c| c['adapter']}.compact.uniq
diff --git a/app/models/attachment.rb b/app/models/attachment.rb
index d1d7c6d..8037666 100644
--- a/app/models/attachment.rb
+++ b/app/models/attachment.rb
@@ -47,10 +47,10 @@ class Attachment < ActiveRecord::Base
                                             "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id")
 
   cattr_accessor :storage_path
-  @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
+  @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Redmine.root, "files")
 
   cattr_accessor :thumbnails_storage_path
-  @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
+  @@thumbnails_storage_path = File.join(Redmine.root, "tmp", "thumbnails")
 
   before_create :files_to_final_location
   after_destroy :delete_from_disk
diff --git a/bin/redmine-instances b/bin/redmine-instances
new file mode 100755
index 0000000..5bf3159
--- /dev/null
+++ b/bin/redmine-instances
@@ -0,0 +1,280 @@
+#!/bin/sh
+
+# Copyright (C) 2015 Antonio Terceiro
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+set -eu
+
+REDMINE_INSTANCES_OWNERSHIP=$(stat -c %U:%G $(dirname $0/..))
+REDMINE_INSTANCES_FOLLOW_FHS=""
+REDMINE_INSTANCES_ROOT="$(readlink -f $(dirname $0)/..)/instances"
+
+if [ -r /etc/default/redmine ]; then
+  . /etc/default/redmine
+fi
+
+usage() {
+  local rc="${1:-0}"
+  local program="$(basename $0)"
+  echo "usage: $program list"
+  echo "usage: $program create INSTANCE"
+  echo "usage: $program remove INSTANCE"
+  echo "usage: $program help"
+  exit $rc
+}
+
+# map directories support for placing the files in the correct locations
+# according to the FHS (http://www.pathname.com/fhs/). Required for official
+# Debian packages.
+fhs_directory_mapping() {
+  if [ -n "$REDMINE_INSTANCES_FOLLOW_FHS" ]; then
+    local instance="$1"
+    local dir="$2"
+    case "$dir" in
+      config)
+        echo "/etc/redmine/$instance"
+        ;;
+      log)
+        echo "/var/log/redmine/$instance"
+        ;;
+      files)
+        echo "/var/lib/redmine/$instance/files"
+        ;;
+      tmp)
+        echo "/var/cache/redmine/$instance"
+        ;;
+    esac
+  fi
+}
+
+make_instance_directory() {
+  local instance="$1"
+  local dirname="$2"
+  local target="$REDMINE_INSTANCES_ROOT/$instance/$dirname"
+  local dir=$(fhs_directory_mapping "$instance" "$dirname")
+  if [ -z "$dir" ]; then
+    dir="$target"
+  fi
+
+  mkdir -p "$dir"
+  chown -R "$REDMINE_INSTANCES_OWNERSHIP" "$dir"
+
+  if [ "$dir" != $target ]; then
+    mkdir -p $(dirname "$target")
+    ln -sfT "$dir" "$target"
+  fi
+}
+
+call_pager() {
+  if [ -t 1 ]; then
+    man -l -
+  else
+    cat
+  fi
+}
+
+if [ $# -lt 1 ]; then
+  usage 1
+fi
+cmd="$1"
+shift
+
+case "$cmd" in
+  list)
+    if [ -d "$REDMINE_INSTANCES_ROOT" ]; then
+      ls -1 "$REDMINE_INSTANCES_ROOT"
+    fi
+    ;;
+
+  create)
+    instance="${1:-}"
+    [ -n "$instance" ] || usage 1
+
+    make_instance_directory "$instance" config
+    make_instance_directory "$instance" log
+    make_instance_directory "$instance" files
+    make_instance_directory "$instance" tmp
+    ;;
+
+  remove)
+    instance="${1:-}"
+    [ -n "$instance" ] || usage 1
+
+    printf "Are you sure you want to remove instance $instance? There is no going back [y/N] "
+    read confirm
+    if [ "$confirm" = 'y' -o "$confirm" = 'Y' ]; then
+      rm -rf "$REDMINE_INSTANCES_ROOT/$instance"
+    else
+      echo "Aborted."
+    fi
+    ;;
+
+  help)
+    sed -e '1,/<<DOCUMENTATION$/d; /^DOCUMENTATION/d' "$0" \
+      | pod2man --name='redmine-instances' --center "" --release "" - \
+      | call_pager
+    ;;
+
+  *)
+    if [ -n "$cmd" ]; then
+      echo "E: invalid command: $command"
+    fi
+    usage 1
+    ;;
+esac
+
+exit
+: <<DOCUMENTATION
+=encoding UTF-8
+
+=head1 redmine-instances
+
+redmine-instances - manage multiple instances in a single Redmine install
+
+=head1 SYNOPSIS
+
+B<redmine-instances> I<COMMAND> [I<INSTANCE>]
+
+See "I<THE REDMINE MULTITENANCY SYSTEM>" below for a description of how it all
+works.
+
+=head1 COMMANDS
+
+=head2 list
+
+Lists the existing instances.
+
+=head2 create INSTANCE
+
+Creates an instance. This command is idempotent, so calling it multiple times
+is not a problem. Not only that, but existing instances will also be repaired
+and gain the expected directory structure.
+
+=head2 remove INSTANCE
+
+Removes an instance. After this command finished, all files in the instance
+directory will be removed. Any MySQL/PostgreSQL databases will I<not> be
+removed, and must be removed manually.
+
+=head2 help
+
+Displays this documentation.
+
+=head1 THE REDMINE MULTITENANCY SYSTEM
+
+The Redmine multitenancy system is implemented in such a way that it is
+unobstrusive: if you don't do anything to configure multiple instances, Redmine
+will just work as it normally does.
+
+To run redmine against a specific instance, you must set the
+I<REDMINE_INSTANCE> environment variable for Redmine processes. Since such
+environment variable is queried at compile times in some parts of the Redmine
+codebase, this means that multiple Redmine instances must be handled as
+I<separate processes>.
+
+When Redmine is started with the I<REDMINE_INSTANCE> environment variable set
+to an instance name, then a few parameters will be adjusted:
+
+I<Logs> will be stored in /path/to/redmine/instances/I<$REDMINE_INSTANCE>/log/
+
+I<Database configuration> will be read from
+/path/to/redmine/instances/I<$REDMINE_INSTANCE>/config/database.yml
+
+I<Temporary files>, such as caches, will be stored in
+/path/to/redmine/instances/I<$REDMINE_INSTANCE>/tmp/
+
+I<File attachments> will be stored in
+/path/to/redmine/instances/I<$REDMINE_INSTANCE>/files/
+
+=head1 NOTES FOR DEBIAN GNU/LINUX
+
+When using the official redmine Debian package, the directories mentioned above
+will actually be symbolic links to the expected locations according to FHS,
+namely:
+
+I<config> will be a link to /etc/redmine/I<$REDMINE_INSTANCE>/
+
+I<log> will be a link /var/log/redmine/I<$REDMINE_INSTANCE>/
+
+I<tmp> will be a link /var/cache/redmine/I<$REDMINE_INSTANCE>/
+
+I<files> will be stored directly in /var/lib/redmine/I<$REDMINE_INSTANCE>/files/
+
+Users of the Debian package can also find B<redmine-instances> in I<$PATH>,
+and can call it directly (as root) from any directory.
+
+=head1 EXAMPLE: CREATING AND USING A NEW INSTANCE
+
+=head2 Creating the directory structure
+
+  $ cd /path/to/redmine
+  $ ./bin/redmine-instances create myinstance
+  $ edit instances/myinstance/config/database.yml
+  [...]
+  $ export REDMINE_INSTANCE=myinstance
+  $ bundle exec rake db:migrate
+  $ bundle exec rake redmine:load_default_data
+  $ bundle exec rake generate_secret_token
+
+=head2 Web server configuration
+
+For Passenger with Apache, you will want something like the example below. Note
+the I<PassengerAppGroupName>: it must be set to a different value for each of
+your Redmine instances, and PassengerAppGroupName will make sure that all your
+instances are isolated from each other.
+
+  <VirtualHost *:80>
+    ServerName my.domain.name
+    RailsEnv production
+    SetEnv REDMINE_INSTANCE "myinstance"
+    PassengerAppGroupName redmine_myinstance
+    PassengerDefaultUser www-data
+    DocumentRoot /path/to/redmine/public
+    <Directory "/path/to/redmine/public">
+      Allow from all
+      Options -MultiViews
+      Require all granted
+    </Directory>
+  </VirtualHost>
+
+=head1 SEE ALSO
+
+Filesystem Hierarchy Standard (FHS), I<http://www.pathname.com/fhs/>.
+
+=head1 COPYRIGHT AND AUTHORS
+
+B<redmine-instances> is Copyright (C) 2015, Antonio Terceiro.
+
+The Multi-Tenancy support for Redmine was originally developed for Debian
+GNU/Linux, and is Copyright (C) 2014-2015 Antonio Terceiro, 2011-2014 Jérémy
+Lal, 2011-2014 Ondrej Surý.
+
+B<Redmine> is Copyright (C) 2006-2015, Jean-Philippe Lang.
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; either version 2 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
+Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+DOCUMENTATION
diff --git a/config/application.rb b/config/application.rb
index 0f75247..b659f30 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -65,6 +65,7 @@ module RedmineApp
 
     config.session_store :cookie_store, :key => '_redmine_session'
 
+    instance_eval File.read(File.join(File.dirname(__FILE__), 'multitenancy_environment.rb'))
     if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
       instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
     end
diff --git a/config/multitenancy_environment.rb b/config/multitenancy_environment.rb
new file mode 100644
index 0000000..f3d9248
--- /dev/null
+++ b/config/multitenancy_environment.rb
@@ -0,0 +1,42 @@
+# Copyright (C) 2014-2015 Antonio Terceiro
+# Copyright (C) 2011-2014 Jérémy Lal
+# Copyright (C) 2011-2014 Ondřej Surý
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require 'redmine/multi_tenancy'
+
+if Redmine.tenant?
+
+  # per-instance tmp/local cache directories
+  config.paths['tmp'] = Redmine.root.join('tmp').to_s
+  config.cache_store = :file_store, Redmine.root.join('tmp', 'cache').to_s
+
+  # per-instance logs
+  config.paths['log'] = Redmine.root.join('log', Rails.env + '.log').to_s
+
+  # per-instance database configuration
+  config.paths['config/database'] = Redmine.root.join('config', 'database.yml').to_s
+
+  # per-instance session cookie
+  path = ENV.fetch('RAILS_RELATIVE_URL_ROOT', '/')
+  key = Redmine.name || '_redmine_session'
+  config.session_store :cookie_store, :key => key, :path => path
+  secret_file = Redmine.root.join('config', 'secret_key.txt')
+  if File.exists?(secret_file)
+    config.secret_key_base = File.read(secret_file).strip
+  end
+
+end
diff --git a/lib/plugins/open_id_authentication/lib/open_id_authentication.rb b/lib/plugins/open_id_authentication/lib/open_id_authentication.rb
index 48be8dd..1bcbbe4 100644
--- a/lib/plugins/open_id_authentication/lib/open_id_authentication.rb
+++ b/lib/plugins/open_id_authentication/lib/open_id_authentication.rb
@@ -25,7 +25,7 @@ module OpenIdAuthentication
       OpenID::Store::Memory.new
     when :file
       require 'openid/store/filesystem'
-      OpenID::Store::Filesystem.new(Rails.root.join('tmp/openids'))
+      OpenID::Store::Filesystem.new(Redmine.root.join('tmp/openids'))
     when :memcache
       require 'memcache'
       require 'openid/store/memcache'
diff --git a/lib/redmine/configuration.rb b/lib/redmine/configuration.rb
index 9109a00..3d93ff7 100644
--- a/lib/redmine/configuration.rb
+++ b/lib/redmine/configuration.rb
@@ -32,7 +32,7 @@ module Redmine
       # * <tt>:file</tt>: the configuration file to load (default: config/configuration.yml)
       # * <tt>:env</tt>: the environment to load the configuration for (default: Rails.env)
       def load(options={})
-        filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml')
+        filename = options[:file] || File.join(Redmine.root, 'config', 'configuration.yml')
         env = options[:env] || Rails.env
 
         @config = @defaults.dup
@@ -104,7 +104,7 @@ module Redmine
       end
 
       def load_deprecated_email_configuration(env)
-        deprecated_email_conf = File.join(Rails.root, 'config', 'email.yml')
+        deprecated_email_conf = File.join(Redmine.root, 'config', 'email.yml')
         if File.file?(deprecated_email_conf)
           warn "Storing outgoing emails configuration in config/email.yml is deprecated. You should now store it in config/configuration.yml using the email_delivery setting."
           @config.merge!({'email_delivery' => load_from_yaml(deprecated_email_conf, env)})
diff --git a/lib/redmine/export/pdf.rb b/lib/redmine/export/pdf.rb
index e3861b6..31ee580 100644
--- a/lib/redmine/export/pdf.rb
+++ b/lib/redmine/export/pdf.rb
@@ -27,7 +27,7 @@ module Redmine
         attr_accessor :footer_date
 
         def initialize(lang, orientation='P')
-          @@k_path_cache = Rails.root.join('tmp', 'pdf')
+          @@k_path_cache = Redmine.root.join('tmp', 'pdf')
           FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
           set_language_if_valid lang
           super(orientation, 'mm', 'A4')
diff --git a/lib/redmine/multi_tenancy.rb b/lib/redmine/multi_tenancy.rb
new file mode 100644
index 0000000..0d100f7
--- /dev/null
+++ b/lib/redmine/multi_tenancy.rb
@@ -0,0 +1,43 @@
+# Copyright (C) 2014-2015 Antonio Terceiro
+# Copyright (C) 2011-2014 Jérémy Lal
+# Copyright (C) 2011-2014 Ondřej Surý
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+module Redmine
+
+  class << self
+
+    def tenant
+      # X_DEBIAN_SITEID is kept here for backwards compatibility with existing
+      # Debian installations.
+      ENV['X_DEBIAN_SITEID'] || ENV['REDMINE_INSTANCE']
+    end
+
+    def tenant?
+      !tenant.nil?
+    end
+
+    def root
+      if tenant
+        Pathname.new(File.join(Rails.root, 'instances', tenant))
+      else
+        Rails.root
+      end
+    end
+
+  end
+
+end
diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb
index 98683b0..f525bb7 100644
--- a/lib/redmine/scm/adapters/abstract_adapter.rb
+++ b/lib/redmine/scm/adapters/abstract_adapter.rb
@@ -218,7 +218,7 @@ module Redmine
           if @stderr_log_file.nil?
             writable = false
             path = Redmine::Configuration['scm_stderr_log_file'].presence
-            path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
+            path ||= Redmine.root.join("log/#{Rails.env}.scm.stderr.log").to_s
             if File.exists?(path)
               if File.file?(path) && File.writable?(path) 
                 writable = true
diff --git a/lib/tasks/initializers.rake b/lib/tasks/initializers.rake
index 6da60c1..eb96177 100644
--- a/lib/tasks/initializers.rake
+++ b/lib/tasks/initializers.rake
@@ -15,7 +15,7 @@ file 'config/initializers/secret_token.rb' do
 # change this key, all old sessions will become invalid! Make sure the
 # secret is at least 30 characters and all random, no regular words or
 # you'll be exposed to dictionary attacks.
-RedmineApp::Application.config.secret_key_base = '#{secret}'
+RedmineApp::Application.config.secret_key_base = [Redmine.tenant, '#{secret}'].compact.join(':')
 EOF
   end
 end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/redmine.git



More information about the Pkg-ruby-extras-commits mailing list