Skip to content

Commit

Permalink
Zero downtime migration
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitryDrobotov committed Sep 10, 2017
1 parent 7580344 commit 4d2ac86
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 0 deletions.
68 changes: 68 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,74 @@ end
migrate_up!
----

== Zero Downtime Migration

=== ActiveRecord

Create new columns in database.

[source,ruby]
----
class Migration1 < ActiveRecord::Migration
def up
add_column :users, :encrypted_new_ssn
add_column :users, :encrypted_new_ssn_iv
end
end
----

Add `attr_encrypted` for new columns, `include Transcryptor::ActiveRecord::ZeroDowntime`, and `transcryptor_migrate :old_attribute_name, :new_attribute_name`.

[source,ruby]
----
class User < ActiveRecord::Base
include Transcryptor::ActiveRecord::ZeroDowntime
attr_encrypted :ssn, key: '1qwe1qwe1qwe1qwe1qwe1qwe1qwe1qwe', algorithm: 'aes-256-cbc'
attr_encrypted :new_ssn, key: '2asd2asd2asd2asd2asd2asd2asd2asd', algorithm: 'aes-256-gcm'
transcryptor_migrate :ssn, :new_ssn
end
----

Create `rake` task for zero downtime migration. Or any other way you prefer.

[source,ruby]
----
namespace :zero_downtime
desc 'migrate attr_encrypted for User'
task user: :environment do
User.find_each { |user| user.save! }
end
end
----

Remove & rename columns in database after finishing of `rake` task.

[source,ruby]
----
class Migration2 < ActiveRecord::Migration
def up
remove_column :users, :encrypted_ssn
remove_column :users, :encrypted_ssn_iv
rename_column :users, :encrypted_new_ssn, :encrypted_ssn
rename_column :users, :encrypted_new_ssn_iv, :encrypted_ssn_iv
end
end
----

Move `attr_encrypted` configuration to original attribute and remove all migration code.

[source,ruby]
----
class User < ActiveRecord::Base
attr_encrypted :ssn, key: '2asd2asd2asd2asd2asd2asd2asd2asd', algorithm: 'aes-256-gcm'
end
----

Done!

== Default Options

Default options for old and new configuration are absolutelly the same as it is defined in `attr_encrypted` gem.
Expand Down
1 change: 1 addition & 0 deletions lib/transcryptor/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ module Transcryptor::ActiveRecord

require 'transcryptor/active_record/adapter'
require 'transcryptor/active_record/re_encrypt_statement'
require 'transcryptor/active_record/zero_downtime'
35 changes: 35 additions & 0 deletions lib/transcryptor/active_record/zero_downtime.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module Transcryptor::ActiveRecord::ZeroDowntime
def self.included(base)
base.extend ClassMethods

base.class_eval do
after_initialize :transcryptor_migrate

@transcryptor_migrate_attributes = []
class << self
attr_reader :transcryptor_migrate_attributes
end
end
end

def transcryptor_migrate
self.class.transcryptor_migrate_attributes.each do |attribute|
send("#{attribute[:new]}=", send(attribute[:old]))
end
end

module ClassMethods
def transcryptor_migrate(old_attribute, new_attribute)
@transcryptor_migrate_attributes << {old: old_attribute, new: new_attribute}

options = attr_encrypted_options.merge(encrypted_attributes[old_attribute])
encrypted_attribute_name = "#{options[:prefix]}#{old_attribute}#{options[:suffix]}"

define_method("#{old_attribute}=") do |value|
send("#{encrypted_attribute_name}=", encrypt(old_attribute, value))
instance_variable_set("@#{old_attribute}", value)
send("#{new_attribute}=", value)
end
end
end
end
33 changes: 33 additions & 0 deletions spec/transcryptor/active_record/zero_downtime_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require 'spec_helper'
require 'securerandom'

ActiveRecord::Base.connection.create_table(:active_record_zero_downtime_specs) do |t|
t.string :encrypted_column_1
t.string :encrypted_column_1_iv
t.string :encrypted_new_column_1
t.string :encrypted_new_column_1_iv
end

class ActiveRecordZeroDowntimeSpec < ActiveRecord::Base
include Transcryptor::ActiveRecord::ZeroDowntime

attr_encrypted :column_1, key: '1qwe1qwe1qwe1qwe1qwe1qwe1qwe1qwe'
attr_encrypted :new_column_1, key: '2asd2asd2asd2asd2asd2asd2asd2asd'

transcryptor_migrate :column_1, :new_column_1
end

describe Transcryptor::ActiveRecord::ZeroDowntime do
let(:instance) { ActiveRecordZeroDowntimeSpec.create(column_1: 'test') }

it 'assigns encrypted data for both attributes' do
expect(instance.new_column_1).to eq('test')
end

it 'changes new attribute on change of old' do
instance.column_1 = 'another'
expect(instance.new_column_1).to eq('another')
end
end

0 comments on commit 4d2ac86

Please sign in to comment.