#!/usr/bin/env ruby require_relative "../config/environment" class EnactTenanting CURRENT_DOMAIN = "https://fizzy.37signals.com" TENANTED_DOMAIN = Rails.env.local? ? "http://%{tenant}.fizzy.localhost:3006" : "https://%{tenant}.fizzy.37signals.com" PROJECT_MAPPING = { 693169841 => "37s", # Writebook improvements 693169850 => "37s", # Fizzy: Issues 693169853 => "37s", # Fizzy: 💡 IDEAS 693169858 => "37s", # Know It All: 💡 IDEAS 693169859 => "37s", # Cycle 1: BC4 Client access for templates 693169860 => "37s", # Know It All: Issues 693169862 => "37s", # Active Record Tenanting 693169863 => "37s", # Cycle 1: BC4 Hilltop View 693169865 => "37s", # [BC iOS] Search refactor 693169867 => "37s", # File icons refactor 693169870 => "37s", # HEY Preload Replies 693169872 => "37s", # [BC4 Android] Search Refactor 693169873 => "37s", # BC4 693169874 => "37s", # [HEY] ContentFilter pipeline rewrite 693169875 => "37s", # [Hey Calendar Android] Search 693169876 => "37s", # [HEY Calendar iOS] Search 693169877 => "37s", # [BC4] Turbo for comment and answer forms 693169842 => "dev", # Foobar 693169843 => "dev", # Test 693169851 => "dev", # Kevin 693169861 => "dev", # Testing space 693169869 => "dev", # Flower ideas 693169871 => "dev", # Mike's TODOs 693169856 => "qa" # QA Exploration } TENANTS = PROJECT_MAPPING.values.uniq attr_reader :creation_counter, :update_counter def initialize @creation_counter = {} @update_counter = {} end def up setup destroy_tenants safety_check copy_accounts_et_al copy_buckets_et_al copy_bubbles_et_al copy_filters copy_active_storage cross_check update_action_text_urls pp [ "updated:", update_counter ] hardlink_active_storage end def setup ApplicationRecord.connects_to shards: { untenanted: { reading: :primary_original } } end def with_original_db(&block) ApplicationRecord.connected_to(role: :reading, shard: :untenanted, &block) end def safety_check buckets = with_original_db { Bucket.all.to_a } unless buckets.map(&:id).sort == PROJECT_MAPPING.keys.sort unknown_buckets = buckets.map(&:id) - PROJECT_MAPPING.keys missing_buckets = PROJECT_MAPPING.keys - buckets.map(&:id) raise "Surprising buckets. unknown #{unknown_buckets.inspect}, missing #{missing_buckets.inspect}" end end def copy_accounts_et_al account = with_original_db { Account.first } users = with_original_db { User.all.to_a } workflows = with_original_db { account.workflows.to_a } workflow_stages = with_original_db { workflows.flat_map(&:stages) } tags = with_original_db { account.tags.to_a } TENANTS.each do |tenant| ApplicationRecord.with_tenant(tenant) do Account.create! name: tenant, id: account.id upsert_all users upsert_all workflows upsert_all workflow_stages upsert_all tags end end end def copy_buckets_et_al PROJECT_MAPPING.each do |bucket_id, tenant| bucket = with_original_db { Bucket.where(id: bucket_id).first } accesses = with_original_db { bucket.accesses.to_a } subscriptions = with_original_db { bucket.subscriptions.to_a } ApplicationRecord.with_tenant(tenant) do upsert_all bucket upsert_all accesses upsert_all subscriptions end end end def copy_bubbles_et_al PROJECT_MAPPING.each do |bucket_id, tenant| bubbles = with_original_db { Bucket.find(bucket_id).bubbles.to_a } assignments = with_original_db { bubbles.flat_map(&:assignments) } pops = with_original_db { bubbles.filter_map(&:pop) } notifications = with_original_db { bubbles.flat_map(&:notifications) } events = with_original_db { bubbles.flat_map(&:events) } taggings = with_original_db { bubbles.flat_map(&:taggings) } watches = with_original_db { bubbles.flat_map(&:watches) } messages = with_original_db { bubbles.flat_map(&:messages) } messageables = with_original_db { messages.filter_map(&:messageable) } comments = messageables.select { _1.is_a?(Comment) } markdowns = with_original_db { comments.map(&:markdown_body) } reactions = with_original_db { comments.flat_map(&:reactions) } ApplicationRecord.with_tenant(tenant) do upsert_all bubbles upsert_all assignments upsert_all pops upsert_all taggings upsert_all watches messageables.group_by(&:class).each do |klass, klass_messageables| upsert_all klass_messageables end upsert_all markdowns upsert_all reactions upsert_all messages upsert_all events upsert_all notifications end end end def copy_filters creation_counter["Filter"] = 0 TENANTS.each do |tenant| buckets = ApplicationRecord.with_tenant(tenant) { Bucket.all.to_a } filters = with_original_db do Filter.all.filter_map do |filter| # hah no pun intended next unless filter.buckets.empty? || buckets.any? { |b| filter.buckets.include?(b) } filter.attributes.merge(filter.as_params) end end ApplicationRecord.with_tenant(tenant) do filters.each do |attr| Filter.create! attr end end creation_counter["Filter"] += filters.length end end def copy_active_storage attachments = with_original_db { ActiveStorage::Attachment.all.to_a } blobs = with_original_db { ActiveStorage::Blob.all.to_a } TENANTS.each do |tenant| ApplicationRecord.with_tenant(tenant) do upsert_all blobs upsert_all attachments end end end def destroy_tenants TENANTS.each do |tenant| ApplicationRecord.destroy_tenant(tenant) end FileUtils.rm_rf "storage/tenants" end def cross_check pp [ "created:", creation_counter ] with_original_db do assert_count User, User.count * TENANTS.length assert_count Workflow, Workflow.count * TENANTS.length assert_count Workflow::Stage, Workflow::Stage.count * TENANTS.length assert_count Tag, Tag.count * TENANTS.length assert_count Bucket, Bucket.count assert_count Access, Access.count assert_count Subscription, Subscription.count assert_count Bubble, Bubble.count assert_count Assignment, Assignment.count assert_count Pop, Pop.count assert_count EventSummary, EventSummary.count assert_count Comment, Comment.count assert_count Reaction, Reaction.count assert_count Message, Message.count assert_count Tagging, Tagging.count assert_count Watch, Watch.count # we're only copying the markdown records that are still accessible. assert_count ActionText::Markdown, Comment.count assert_count Event, Event.count assert_count Notification, Notification.count assert_count ActiveStorage::Attachment, ActiveStorage::Attachment.count * TENANTS.length assert_count ActiveStorage::Blob, ActiveStorage::Blob.count * TENANTS.length raise "Filter count is off" unless creation_counter["Filter"] >= Filter.count end end def update_action_text_urls update_counter["ActionText::Markdown"] = 0 TENANTS.each do |tenant| tenanted_domain = sprintf(TENANTED_DOMAIN, tenant: tenant) ApplicationRecord.with_tenant(tenant) do ActionText::Markdown.all.each do |markdown| content = markdown.content next unless content =~ %r{#{CURRENT_DOMAIN}/u/} content.gsub!(%r{#{CURRENT_DOMAIN}/u/}, "#{tenanted_domain}/u/") markdown.update_column :content, content update_counter["ActionText::Markdown"] += 1 end end end end def hardlink_active_storage files = Dir.glob("storage/files/*/*/*").map { _1.split("/")[2..].join("/") } TENANTS.each do |tenant| ApplicationRecord.with_tenant(tenant) do destdir = ActiveStorage::Blob.service.root files.each do |file| FileUtils.mkdir_p File.join(destdir, File.dirname(file)) FileUtils.ln File.join("storage/files", file), File.join(destdir, file) end end end end def upsert_all(originals) originals = Array(originals) return if originals.empty? klass = originals.first.class result = klass.upsert_all(originals.collect(&:attributes)) raise "Error upserting" unless result.rows.length == originals.length creation_counter[klass.name] ||= 0 creation_counter[klass.name] += originals.length nil end def assert_count(klass, expected) actual = creation_counter[klass.name] unless actual == expected raise "#{klass} count is off: expected #{expected}, got #{actual}" end end end EnactTenanting.new.up exit 0