Feature flagging with rails engines

57
feature flagging with Ruby on Rails engines Enrico Teotti -- @agenteo

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 of Feature flagging with rails engines

feature flagging with Ruby on Rails engines

Enrico Teotti -- @agenteo

admin user interface

public user interface

domain logic

requirementrun the private and public portions of the app

on separate servers

service oriented architecture

first proposal

two apps sharing components via engines

second proposal

local rails engines components

third proposal

public_uiadmin_ui

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

admin user interface

public user interface

domain logic

mkdir components

rails plugin new admin_ui --mountable --dummy=spec/dummy -O -T

mv admin_ui components

admin user interface

public user interface

domain logic

admin_ui

proceeding without automated tests

could drive you crazy

shared user interface (preview)

shared domain

admin_ui

Rails.application.routes.draw do

# ... public routes here

mount AdminUi::Engine => "/"

end

config/routes.rb

gem 'admin_ui', path: 'components/admin_ui'

Gemfile

public user interface

admin_ui

public_ui

rails plugin new public_ui --mountable --dummy=spec/dummy -O -T

gem 'admin_ui', path: 'components/admin_ui'

gem 'public_ui', path: 'components/public_ui'

Gemfile

public_uiadmin_ui

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

the two engines are now glued together!

admin_ui public_ui

domain_logic

rails plugin new domain_logic --dummy-path=spec/dummy --mountable

-O -T

admin_ui public_ui

shared_ui domain_logic

rails plugin new shared_ui --dummy-path=spec/dummy --mountable -O

-T

<%= 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.

common pitfalls in Rails engines land

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

the test dummy app

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

testing multiple engines

#!/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

module AdminUi

class CargosController < AdminUi::ApplicationController

module AdminUi

class VoyagesController < ApplicationController

module AdminUi

class VoyagesController < ApplicationController

scaffolding and other generators

rails generate scaffold_controller admin_ui/cargo source

destination weight --model-name=DomainLogic::Cargo --orm=mongoid

-t=''

require_dependency "admin_ui/domain_logic/application_controller"

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]