feature flagging with rails engines
DESCRIPTION
Recently I was asked to separate the public from the administration portion of a web application and deploy them to different servers. I'll present a Rails application where the admin and public portions are activated based on an application running mode flag. In order to achieve this the application components are separated in Rails engines, and I'll go trough some common pitfalls with this approach as well as discussing its long term benefits.TRANSCRIPT
Rails.application.routes.draw do
case AppRunningMode.value
when :admin
mount AdminUi::Engine => "/"
when :public
mount PublicUi::Engine => "/"
else
mount AdminUi::Engine => "/"
mount PublicUi::Engine => "/"
end
end
config/routes.rb
RUNNING_MODE=public rails
s
RUNNING_MODE=admin rails s
http://worldwideshipping-super-secret-domain.com/admin
http://worldwideshipping.com
rails s
http://localhost:3000
● are ruby gems● are special ruby gems that provide extra
behaviour (models, views, routes, rake tasks) to a Rails application
● they can be hosted on a gemserver or they can live inside your repository
● can be tested in isolation
rails engines
Rails.application.routes.draw do
# ... public routes here
mount AdminUi::Engine => "/"
end
config/routes.rb
Gem::Specification.new do |s|
# ... other fields up here
s.name = "public_ui"
s.add_dependency "rails", "~> 4.1.1"
s.add_dependency 'jquery-rails'
s.add_dependency 'mongoid'
s.add_runtime_dependency "admin_ui"
s.add_development_dependency 'byebug'
s.add_development_dependency 'database_cleaner'
s.add_development_dependency 'rspec-rails', '2.99.0'
s.add_development_dependency 'capybara'
s.add_development_dependency 'poltergeist'
end
public_ui.gemspec
Gem::Specification.new do |s|
# ... other fields up here
s.name = "public_ui"
s.add_dependency "rails", "~> 4.1.1"
s.add_dependency 'jquery-rails'
s.add_dependency 'mongoid'
s.add_runtime_dependency "admin_ui"
s.add_development_dependency 'byebug'
s.add_development_dependency 'database_cleaner'
s.add_development_dependency 'rspec-rails', '2.99.0'
s.add_development_dependency 'capybara'
s.add_development_dependency 'poltergeist'
end
public_ui.gemspec
Rails.application.routes.draw do
case AppRunningMode.value
when :admin
mount AdminUi::Engine => "/"
when :public
mount PublicUi::Engine => "/"
else
mount AdminUi::Engine => "/"
mount PublicUi::Engine => "/"
end
end
config/routes.rb
<%= render partial: 'shared_ui/cargos/show' %>
admin_ui/app/views/cargo_preview/show.html.erb
<%# admin console stuff here %>
<%= render partial: 'shared_ui/cargos/show' %>
<%# admin spaceship here %>
public_ui/app/views/cargos/show.html.erb
gem 'domain_logic', path: 'components/domain_logic'
gem 'shared_ui', path: 'components/shared_ui'
gem 'admin_ui', path: 'components/admin_ui'
gem 'public_ui', path: 'components/public_ui'
Gemfile
#gem 'domain_logic', path: 'components/domain_logic'
#gem 'shared_ui', path: 'components/shared_ui'
gem 'admin_ui', path: 'components/admin_ui'
gem 'public_ui', path: 'components/public_ui'
Gemfile
$ bundle
Resolving dependencies...
Could not find gem 'shared_ui (>= 0) ruby', which is required by
gem 'admin_ui (>= 0) ruby', in any of the sources.
Gem::Specification.new do |s|
# ... other fields up here
s.name = "admin_ui"
s.add_dependency "rails", "~> 4.1.1"
s.add_dependency 'jquery-rails'
s.add_dependency 'mongoid'
s.add_dependency 'faraday'
s.add_dependency "domain_logic"
s.add_dependency "shared_ui"
s.add_development_dependency 'byebug'
s.add_development_dependency 'database_cleaner'
s.add_development_dependency 'rspec-rails', '2.99.0'
s.add_development_dependency 'vcr'
s.add_development_dependency 'webmock'
s.add_development_dependency 'capybara'
s.add_development_dependency 'poltergeist'
end
admin_ui.gemspec
Gem::Specification.new do |s|
# ... other fields up here
s.name = "admin_ui"
s.add_dependency "rails", "~> 4.1.1"
s.add_dependency 'jquery-rails'
s.add_dependency 'mongoid'
s.add_dependency 'faraday'
s.add_dependency "domain_logic"
s.add_dependency "shared_ui"
s.add_development_dependency 'byebug'
s.add_development_dependency 'database_cleaner'
s.add_development_dependency 'rspec-rails', '2.99.0'
s.add_development_dependency 'vcr'
s.add_development_dependency 'webmock'
s.add_development_dependency 'capybara'
s.add_development_dependency 'poltergeist'
end
admin_ui/admin_ui.gemspec
source "https://rubygems.org"
gem 'domain_logic', path: '../domain_logic'
gem 'shared_ui', path: '../shared_ui'
# Declare your gem's dependencies in admin_ui.gemspec.
# Bundler will treat runtime dependencies like base
dependencies, and
# development dependencies will be added by default to
the :development group.
gemspec
admin_ui/Gemfile
require File.expand_path("../dummy/config/environment", __FILE__)
admin_ui/spec/rails_helper.rb
admin_ui/spec/dummy/config/boot.rb
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
Rails.application.routes.draw do
mount AdminUi::Engine => "/admin_ui"
end
admin_ui/spec /dummy/config/routes.rb
Rails.application.routes.draw do
mount AdminUi::Engine => "/admin_ui"
end
admin_ui/spec /dummy/config/routes.rb
#!/bin/bash
unset BUNDLE_GEMFILE
result=0
if [ "$CI" == "true" ]; then
BUNDLE_PATH="$HOME/vendor/bundle"
fi
for test_script in $(find . -name test.sh); do
pushd `dirname $test_script̀ > /dev/null
./test.sh
result+=$?
popd > /dev/null
done
if [ $result -eq 0 ]; then
echo "SUCCESS"
else
echo "FAILURE"
fi
exit $resulthttps://github.com/shageman/the_next_big_thing/blob/master/build.sh
require_dependency "admin_ui/application_controller"
module AdminUi
class CargosController < ApplicationController
rails generate scaffold_controller admin_ui/cargo source
destination weight --model-name=DomainLogic::Cargo --orm=mongoid
-t=''
Failures:
1) Staff booking a cargo booking a cargo fitting a pending voyage
Failure/Error: visit '/admin/cargos'
LoadError:
No such file to load -- admin_ui/domain_logic/application_controller
# ./engines/admin_ui/app/controllers/admin_ui/cargos_controller.rb:1:in `<top (required)>'
# ./spec/features/book_cargo_spec.rb:12:in `block (2 levels) in <top (required)>'
Finished in 0.03067 seconds (files took 1.85 seconds to load)
2 examples, 1 failure
Failures:
1) Staff booking a cargo booking a cargo fitting a pending voyage
Failure/Error: visit '/admin/cargos'
RuntimeError:
Circular dependency detected while autoloading constant AdminUi::AdminUi::CargosHelper
# ./engines/admin_ui/app/controllers/admin_ui/application_controller.rb:2:in `<module:AdminUi>'
# ./engines/admin_ui/app/controllers/admin_ui/application_controller.rb:1:in `<top (required)>'
# ./engines/admin_ui/app/controllers/admin_ui/cargos_controller.rb:1:in `<top (required)>'
# ./spec/features/book_cargo_spec.rb:12:in `block (2 levels) in <top (required)>'
Failures:
1) Staff booking a cargo booking a cargo fitting a pending voyage
Failure/Error: visit '/admin/cargos'
ActionView::Template::Error:
undefined local variable or method `new_domain_logic_cargo_path' for #<#<Class:0x007fb38887bb38>:
0x007fb388873690>
# ./engines/admin_ui/app/views/admin_ui/cargos/index.html.erb:29:in
`_engines_admin_ui_app_views_admin_ui_cargos_index_html_erb__4343322268368929370_70204533119300'
# ./spec/features/book_cargo_spec.rb:12:in `block (2 levels) in <top (required)>'
fang:domain_logic agenteo$ rails generate mongoid:config
/Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/mongoid-4.0.0/lib/rails/generators /mongoid/config/config_generator.rb: 16:in
`app_name': undefined method `parent' for nil:NilClass (NoMethodError)
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/command.rb: 27:in `run'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/invocation.rb: 126:in `invoke_command'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/invocation.rb: 133:in `block in invoke_all'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/invocation.rb: 133:in `each'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/invocation.rb: 133:in `map'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/invocation.rb: 133:in `invoke_all'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/group.rb:232:in `dispatch'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/thor-0.19.1/lib/thor/base.rb:440:in `start'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/railties-4.1.4/lib/rails/generators.rb: 157:in `invoke'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/railties-4.1.4/lib/rails/commands/generate.rb: 11:in `<top (required)
>'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/railties-4.1.4/lib/rails/engine/commands.rb: 19:in `require'
from /Users/agenteo/.rvm/gems/ruby-2.1.2@worldwide_shipping /gems/railties-4.1.4/lib/rails/engine/commands.rb: 19:in `<top (required) >'
from bin/rails:12:in `require'
from bin/rails:12:in `<main>'
FOLLOWUP READShttp://guides.rubyonrails.org/engines.html
http://pivotallabs.com/tag/cobra/
https://leanpub.com/cobra
http://teotti.com
Enrico Teotti @agenteo [email protected]