Class: Ekylibre::Tenant

Inherits:
Object
  • Object
show all
Defined in:
lib/ekylibre/tenant.rb

Constant Summary collapse

AGGREGATION_NAME =
'__all__'.freeze

Class Method Summary collapse

Class Method Details

.add(name) ⇒ Object

Adds a tenant in config. No schema are created.


49
50
51
52
# File 'lib/ekylibre/tenant.rb', line 49

def add(name)
  list << name unless list.include?(name)
  write
end

.check!(name, options = {}) ⇒ Object

Tests existence of a tenant in DB and removes it if not exist


19
20
21
22
23
# File 'lib/ekylibre/tenant.rb', line 19

def check!(name, options = {})
  if list.include?(name)
    drop(name, options) unless Apartment.connection.schema_exists? name
  end
end

.clear!Object


222
223
224
225
226
# File 'lib/ekylibre/tenant.rb', line 222

def clear!
  list unless @list
  @list[env] = []
  write
end

.create(name) ⇒ Object

Create a new tenant with tables and co

Raises:


40
41
42
43
44
45
46
# File 'lib/ekylibre/tenant.rb', line 40

def create(name)
  name = name.to_s
  check!(name)
  raise TenantError, 'Already existing tenant' if exist?(name)
  Apartment::Tenant.create(name)
  add(name)
end

.create_aggregation_schema!Object


241
242
243
# File 'lib/ekylibre/tenant.rb', line 241

def create_aggregation_schema!
  create_aggregation_views_schema!
end

.create_aggregation_views_schema!Object


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/ekylibre/tenant.rb', line 245

def create_aggregation_views_schema!
  raise 'No tenant to build an aggregation schema' if list.empty?
  name = AGGREGATION_NAME
  connection = ActiveRecord::Base.connection
  connection.execute("CREATE SCHEMA IF NOT EXISTS #{name};")
  for table in Ekylibre::Schema.tables.keys
    connection.execute "DROP VIEW IF EXISTS #{name}.#{table}"
    columns = Ekylibre::Schema.columns(table)
    queries = list.collect do |tenant|
      "SELECT '#{tenant}' AS tenant_name, " + columns.collect { |c| c[:name] }.join(', ') + " FROM #{tenant}.#{table}"
    end
    query = "CREATE VIEW #{name}.#{table} AS " + queries.join(' UNION ALL ')
    connection.execute(query)
  end
end

.currentObject

Returns the current tenant


26
27
28
29
30
31
# File 'lib/ekylibre/tenant.rb', line 26

def current
  unless name = Apartment::Tenant.current
    raise TenantError, 'No current tenant'
  end
  name
end

.drop(name, options = {}) ⇒ Object

Drop tenant

Raises:


63
64
65
66
67
68
69
70
# File 'lib/ekylibre/tenant.rb', line 63

def drop(name, options = {})
  name = name.to_s
  raise TenantError, "Unexistent tenant: #{name}" unless exist?(name)
  Apartment::Tenant.drop(name) if Apartment.connection.schema_exists? name
  FileUtils.rm_rf private_directory(name) unless options[:keep_files]
  @list[env].delete(name)
  write
end

.drop_aggregation_schema!Object


237
238
239
# File 'lib/ekylibre/tenant.rb', line 237

def drop_aggregation_schema!
  ActiveRecord::Base.connection.execute("CREATE SCHEMA IF NOT EXISTS #{AGGREGATION_NAME};")
end

.dump(name, options = {}) ⇒ Object

Dump database and files data to a zip archive with specific places This archive is database independent


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/ekylibre/tenant.rb', line 89

def dump(name, options = {})
  destination_path = options.delete(:path) || Rails.root.join('tmp', 'archives')
  switch(name) do
    archive_file = destination_path.join("#{name}.zip")
    archive_path = destination_path.join("#{name}-dump")
    tables_path = archive_path.join('tables')
    files_path = archive_path.join('files')

    FileUtils.rm_rf(archive_path)

    FileUtils.mkdir_p(tables_path)
    version = Fixturing.extract(path: tables_path)

    if private_directory.exist?
      FileUtils.mkdir_p(files_path.dirname)
      FileUtils.cp_r(private_directory.to_s, files_path.to_s)
    end

    File.open(archive_path.join('mimetype'), 'wb') do |f|
      f.write 'application/vnd.ekylibre.tenant.archive'
    end

    File.open(archive_path.join('manifest.yml'), 'wb') do |f|
      options.update(
        tenant: name,
        format_version: '2.0',
        database_version: version,
        creation_at: Time.zone.now,
        created_with: "Ekylibre #{Ekylibre::VERSION}"
      )
      f.write options.stringify_keys.to_yaml
    end

    FileUtils.rm_rf(archive_file)
    Zip::File.open(archive_file, Zip::File::CREATE) do |zile|
      Dir.chdir archive_path do
        Dir.glob('**/*').each do |path|
          zile.add(path, archive_path.join(path))
        end
      end
    end

    FileUtils.rm_rf(archive_path)
  end
end

.exist?(name) ⇒ Boolean

Tests existence of a tenant

Returns:

  • (Boolean)

13
14
15
# File 'lib/ekylibre/tenant.rb', line 13

def exist?(name)
  list.include?(name)
end

.listObject


228
229
230
231
232
233
234
235
# File 'lib/ekylibre/tenant.rb', line 228

def list
  unless @list
    @list = (File.exist?(config_file) ? YAML.load_file(config_file) : {})
    @list ||= {}
  end
  @list[env] ||= []
  @list[env]
end

.migrate(name, options = {}) ⇒ Object

Migrate tenant to wanted version


73
74
75
76
77
# File 'lib/ekylibre/tenant.rb', line 73

def migrate(name, options = {})
  switch(name) do
    ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, options[:to])
  end
end

.private_directory(name = nil) ⇒ Object

Returns the private directory of the current tenant If tenant name given, it returns its private_directory


35
36
37
# File 'lib/ekylibre/tenant.rb', line 35

def private_directory(name = nil)
  Ekylibre.root.join('private', name || current)
end

.rename(old, new) ⇒ Object

Raises:


79
80
81
82
83
84
85
# File 'lib/ekylibre/tenant.rb', line 79

def rename(old, new)
  check!(old)
  raise TenantError, "Unexistent tenant: #{name}" unless exist?(old)
  ActiveRecord::Base.connection.execute("ALTER SCHEMA #{old.to_s.inspect} RENAME TO #{new.to_s.inspect};")
  @list[env].delete(old.to_s)
  @list[env] << new.to_s
end

.reset_search_path!Object


261
262
263
# File 'lib/ekylibre/tenant.rb', line 261

def reset_search_path!
  ActiveRecord::Base.connection.schema_search_path = Ekylibre::Application.config.database_configuration[::Rails.env]['schema_search_path']
end

.restore(archive_file, options = {}) ⇒ Object

Restore an archive


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ekylibre/tenant.rb', line 136

def restore(archive_file, options = {})
  code = options[:tenant] || Time.zone.now.to_i.to_s(36) + rand(999_999_999).to_s(36)
  verbose = !options[:verbose].is_a?(FalseClass)

  archive_path = Rails.root.join('tmp', 'archives', "#{code}-restore")
  tables_path = archive_path.join('tables')
  files_path = archive_path.join('files')

  FileUtils.rm_rf(archive_path)
  FileUtils.mkdir_p(archive_path)

  puts "Decompressing #{archive_file.basename} to #{archive_path.basename}...".yellow if verbose
  Zip::File.open(archive_file.to_s) do |zile|
    zile.each do |entry|
      entry.extract(archive_path.join(entry.name))
    end
  end

  puts 'Checking archive...'.yellow if verbose
  raise 'Invalid archive' unless archive_path.join('manifest.yml').exist?

  manifest = YAML.load_file(archive_path.join('manifest.yml')).symbolize_keys
  format_version = manifest[:format_version]
  unless format_version == '2.0'
    raise "Cannot handle this version of archive: #{format_version}"
  end

  unless name = options[:tenant] || manifest[:tenant]
    raise 'No given name for the tenant'
  end

  database_version = manifest[:database_version].to_i
  if database_version > ActiveRecord::Migrator.last_version
    raise 'Too recent archive'
  end

  puts "Resetting tenant #{name}...".yellow if verbose
  drop(name) if exist?(name)
  create(name)

  switch(name) do
    if files_path.exist?
      puts 'Restoring files...'.yellow if verbose
      FileUtils.rm_rf private_directory
      FileUtils.mv files_path, private_directory
    else
      puts 'No files to restore'.yellow if verbose
    end

    puts 'Restoring database and migrating...'.yellow if verbose
    Fixturing.restore(name, version: database_version, path: tables_path, verbose: verbose)
    puts 'Done!'.yellow if verbose
  end

  FileUtils.rm_rf(archive_path)
end

.setup!(name, options = {}) ⇒ Object

Add a tenant in config without creating it Nothing is done if already exist


56
57
58
59
60
# File 'lib/ekylibre/tenant.rb', line 56

def setup!(name, options = {})
  check!(name, options)
  create(name) unless exist?(name)
  switch!(name)
end

.switch(name, &block) ⇒ Object

Change current tenant


194
195
196
197
# File 'lib/ekylibre/tenant.rb', line 194

def switch(name, &block)
  raise 'Need block to use Ekylibre::Tenant.switch' unless block_given?
  Apartment::Tenant.switch(name, &block)
end

.switch!(name) ⇒ Object Also known as: current=


199
200
201
# File 'lib/ekylibre/tenant.rb', line 199

def switch!(name)
  Apartment::Tenant.switch!(name)
end

.switch_default!Object


205
206
207
208
209
210
211
# File 'lib/ekylibre/tenant.rb', line 205

def switch_default!
  if list.empty?
    raise TenantError, 'No default tenant'
  else
    Apartment::Tenant.switch!(list.first)
  end
end

.switch_each(&_block) ⇒ Object

Browse all tenant to make actions on it


214
215
216
217
218
219
220
# File 'lib/ekylibre/tenant.rb', line 214

def switch_each(&_block)
  list.each do |tenant|
    switch(tenant) do
      yield tenant
    end
  end
end