Migrate to flipper

This commit is contained in:
Paul Chavard 2019-07-04 12:36:17 +02:00 committed by Pierre de La Morinerie
parent 28d869e818
commit 65e227c44b
33 changed files with 186 additions and 181 deletions

View file

@ -27,6 +27,9 @@ gem 'devise' # Gestion des comptes utilisateurs
gem 'devise-async' gem 'devise-async'
gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded before rails gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded before rails
gem 'flipflop' gem 'flipflop'
gem 'flipper'
gem 'flipper-active_record'
gem 'flipper-ui'
gem 'fog-openstack' gem 'fog-openstack'
gem 'font-awesome-rails' gem 'font-awesome-rails'
gem 'gon' gem 'gon'

View file

@ -222,6 +222,15 @@ GEM
ffi (1.9.25) ffi (1.9.25)
flipflop (2.4.0) flipflop (2.4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
flipper (0.16.2)
flipper-active_record (0.16.2)
activerecord (>= 3.2, < 6)
flipper (~> 0.16.2)
flipper-ui (0.16.2)
erubis (~> 2.7.0)
flipper (~> 0.16.2)
rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, < 2.1.0)
fog-core (2.1.2) fog-core (2.1.2)
builder builder
excon (~> 0.58) excon (~> 0.58)
@ -725,6 +734,9 @@ DEPENDENCIES
dotenv-rails dotenv-rails
factory_bot factory_bot
flipflop flipflop
flipper
flipper-active_record
flipper-ui
fog-openstack fog-openstack
font-awesome-rails font-awesome-rails
gon gon

View file

@ -12,7 +12,7 @@ class ApplicationController < ActionController::Base
before_action :set_raven_context before_action :set_raven_context
before_action :redirect_if_untrusted before_action :redirect_if_untrusted
before_action :authorize_request_for_profiler before_action :authorize_request_for_profiler
before_action :reject, if: -> { Flipflop.maintenance_mode? } before_action :reject, if: -> { feature_enabled?(:maintenance_mode) }
before_action :staging_authenticate before_action :staging_authenticate
before_action :set_active_storage_host before_action :set_active_storage_host
@ -28,7 +28,7 @@ class ApplicationController < ActionController::Base
end end
def authorize_request_for_profiler def authorize_request_for_profiler
if Flipflop.mini_profiler_enabled? if feature_enabled?(:mini_profiler)
Rack::MiniProfiler.authorize_request Rack::MiniProfiler.authorize_request
end end
end end
@ -77,6 +77,10 @@ class ApplicationController < ActionController::Base
protected protected
def feature_enabled?(feature_name)
Flipper.enabled?(feature_name, current_user)
end
def authenticate_logged_user! def authenticate_logged_user!
if instructeur_signed_in? if instructeur_signed_in?
authenticate_instructeur! authenticate_instructeur!
@ -190,7 +194,7 @@ class ApplicationController < ActionController::Base
def redirect_if_untrusted def redirect_if_untrusted
if instructeur_signed_in? && if instructeur_signed_in? &&
sensitive_path && sensitive_path &&
!Flipflop.bypass_email_login_token? && !feature_enabled?(:instructeur_bypass_email_login_token) &&
!IPService.ip_trusted?(request.headers['X-Forwarded-For']) && !IPService.ip_trusted?(request.headers['X-Forwarded-For']) &&
!trusted_device? !trusted_device?

View file

@ -191,7 +191,7 @@ module Instructeurs
end end
def telecharger_pjs def telecharger_pjs
return head(:forbidden) if !Flipflop.download_as_zip_enabled? || !dossier.attachments_downloadable? return head(:forbidden) if !feature_enabled?(:instructeur_download_as_zip) || !dossier.attachments_downloadable?
files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier) files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)

View file

@ -19,20 +19,6 @@ module Manager
redirect_to manager_administrateur_path(params[:id]) redirect_to manager_administrateur_path(params[:id])
end end
def enable_feature
administrateur = Administrateur.find(params[:id])
params[:features].each do |key, enable|
if enable
administrateur.enable_feature(key.to_sym)
else
administrateur.disable_feature(key.to_sym)
end
end
head :ok
end
def delete def delete
administrateur = Administrateur.find(params[:id]) administrateur = Administrateur.find(params[:id])

View file

@ -6,19 +6,5 @@ module Manager
flash[:notice] = "Instructeur réinvité." flash[:notice] = "Instructeur réinvité."
redirect_to manager_instructeur_path(instructeur) redirect_to manager_instructeur_path(instructeur)
end end
def enable_feature
instructeur = Instructeur.find(params[:id])
params[:features].each do |key, enable|
if enable
instructeur.enable_feature(key.to_sym)
else
instructeur.disable_feature(key.to_sym)
end
end
head :ok
end
end end
end end

View file

@ -6,5 +6,19 @@ module Manager
flash[:notice] = "L'email d'activation de votre compte a été renvoyé." flash[:notice] = "L'email d'activation de votre compte a été renvoyé."
redirect_to manager_user_path(user) redirect_to manager_user_path(user)
end end
def enable_feature
user = User.find(params[:id])
params[:features].each do |key, enable|
if enable
Flipper.enable_actor(key.to_sym, user)
else
Flipper.disable_actor(key.to_sym, user)
end
end
head :ok
end
end end
end end

View file

@ -0,0 +1,5 @@
module FlipperHelper
def feature_enabled?(feature_name)
Flipper.enabled?(feature_name, current_user)
end
end

View file

@ -50,7 +50,7 @@ module ProcedureHelper
private private
TOGGLES = { TOGGLES = {
TypeDeChamp.type_champs.fetch(:integer_number) => :champ_integer_number? TypeDeChamp.type_champs.fetch(:integer_number) => :administrateur_champ_integer_number
} }
def types_de_champ_types def types_de_champ_types
@ -58,7 +58,7 @@ module ProcedureHelper
types_de_champ_types.select! do |tdc| types_de_champ_types.select! do |tdc|
toggle = TOGGLES[tdc.last] toggle = TOGGLES[tdc.last]
toggle.blank? || Flipflop.send(toggle) toggle.blank? || feature_enabled?(toggle)
end end
types_de_champ_types types_de_champ_types

View file

@ -44,7 +44,7 @@ class ApiEntreprise::API
def self.url(resource_name, siret_or_siren) def self.url(resource_name, siret_or_siren)
base_url = [API_ENTREPRISE_URL, resource_name, siret_or_siren].join("/") base_url = [API_ENTREPRISE_URL, resource_name, siret_or_siren].join("/")
if Flipflop.insee_api_v3? if Flipper.enabled?(:insee_api_v3)
base_url += "?with_insee_v3=true" base_url += "?with_insee_v3=true"
end end

View file

@ -1,32 +0,0 @@
module Flipflop::Strategies
class UserPreferenceStrategy < AbstractStrategy
def self.default_description
"Allows configuration of features per user."
end
def switchable?
false
end
def enabled?(feature)
find_current_administrateur&.feature_enabled?(feature) ||
find_current_instructeur&.feature_enabled?(feature)
end
private
def find_current_administrateur
administrateur_id = Current.administrateur&.id
if administrateur_id
Administrateur.find_by(id: administrateur_id)
end
end
def find_current_instructeur
instructeur_id = Current.instructeur&.id
if instructeur_id
Instructeur.find_by(id: instructeur_id)
end
end
end
end

View file

@ -72,23 +72,6 @@ class Administrateur < ApplicationRecord
administrateur administrateur
end end
def feature_enabled?(feature)
Flipflop.feature_set.feature(feature)
features[feature.to_s]
end
def disable_feature(feature)
Flipflop.feature_set.feature(feature)
features.delete(feature.to_s)
save
end
def enable_feature(feature)
Flipflop.feature_set.feature(feature)
features[feature.to_s] = true
save
end
def owns?(procedure) def owns?(procedure)
procedure.administrateurs.include?(self) procedure.administrateurs.include?(self)
end end

View file

@ -66,7 +66,7 @@ class DossierOperationLog < ApplicationRecord
def self.serialize_subject(subject) def self.serialize_subject(subject)
if subject.nil? if subject.nil?
nil nil
elsif !Flipflop.operation_log_serialize_subject? elsif !Flipper.enabled?(:operation_log_serialize_subject)
{ id: subject.id } { id: subject.id }
else else
case subject case subject

View file

@ -180,23 +180,6 @@ class Instructeur < ApplicationRecord
Follow.where(instructeur: self, dossier: dossier).update_all(attributes) Follow.where(instructeur: self, dossier: dossier).update_all(attributes)
end end
def feature_enabled?(feature)
Flipflop.feature_set.feature(feature)
features[feature.to_s]
end
def disable_feature(feature)
Flipflop.feature_set.feature(feature)
features.delete(feature.to_s)
save
end
def enable_feature(feature)
Flipflop.feature_set.feature(feature)
features[feature.to_s] = true
save
end
def young_login_token? def young_login_token?
trusted_device_token = trusted_device_tokens.order(created_at: :desc).first trusted_device_token = trusted_device_tokens.order(created_at: :desc).first
trusted_device_token&.token_young? trusted_device_token&.token_young?

View file

@ -87,6 +87,10 @@ class User < ApplicationRecord
user user
end end
def flipper_id
"User:#{id}"
end
private private
def link_invites! def link_invites!

View file

@ -125,7 +125,7 @@
.col-md-6 .col-md-6
%h4 Options avancées %h4 Options avancées
- if Flipflop.web_hook? - if feature_enabled?(:administrateur_web_hook)
%label{ for: :web_hook_url } Lien de rappel HTTP (webhook) %label{ for: :web_hook_url } Lien de rappel HTTP (webhook)
= f.text_field :web_hook_url, class: 'form-control', placeholder: 'https://callback.exemple.fr/' = f.text_field :web_hook_url, class: 'form-control', placeholder: 'https://callback.exemple.fr/'
%p.help-block %p.help-block

View file

@ -1,14 +1,11 @@
:ruby :ruby
url = if field.resource.class.name == 'Instructeur' group = field.resource.class.name.downcase
enable_feature_manager_instructeur_path(field.resource.id) user = field.resource.user
else url = enable_feature_manager_user_path(user)
enable_feature_manager_administrateur_path(field.resource.id)
end
%table#features %table#features
- admin_features = Flipflop.feature_set.features.reject{ |f| f.group.try(:key) == :production } - Flipper.features.select { |feature| feature.key.start_with?("#{group}_") }.each do |feature|
- admin_features.each do |feature|
%tr %tr
%td= feature.title %td= feature
%td %td
= check_box_tag "enable-feature", "enable", field.data[feature.name], data: { url: url, key: feature.key } = check_box_tag "enable-feature", "enable", feature.enabled?(user), data: { url: url, key: feature.key }

View file

@ -7,7 +7,7 @@
%li %li
= link_to "Uniquement cet onglet", "#", onclick: "window.print()", class: "menu-item menu-link" = link_to "Uniquement cet onglet", "#", onclick: "window.print()", class: "menu-item menu-link"
- if Flipflop.download_as_zip_enabled? && !PiecesJustificativesService.liste_pieces_justificatives(dossier).empty? - if feature_enabled?(:instructeur_download_as_zip) && !PiecesJustificativesService.liste_pieces_justificatives(dossier).empty?
%span.dropdown.print-menu-opener %span.dropdown.print-menu-opener
%button.button.dropdown-button.icon-only %button.button.dropdown-button.icon-only
%span.icon.attachment %span.icon.attachment

View file

@ -1,4 +1,4 @@
- if Flipflop.pre_maintenance_mode? - if feature_enabled?(:pre_maintenance_mode)
.maintenance .maintenance
%span %span
Une opération de maintenance est prévue sur demarches-simplifiees.fr à 23 h 00. La plateforme sera inaccessible pendant une vingtaine de minutes. Une opération de maintenance est prévue sur demarches-simplifiees.fr à 23 h 00. La plateforme sera inaccessible pendant une vingtaine de minutes.

View file

@ -21,7 +21,7 @@
= Gon::Base.render_data(camel_case: true, init: true, nonce: request.content_security_policy_nonce) = Gon::Base.render_data(camel_case: true, init: true, nonce: request.content_security_policy_nonce)
- if Flipflop.xray_enabled? - if feature_enabled?(:xray)
= stylesheet_link_tag :xray = stylesheet_link_tag :xray
%body{ id: content_for(:page_id), class: browser.platform.ios? ? 'ios' : nil } %body{ id: content_for(:page_id), class: browser.platform.ios? ? 'ios' : nil }
@ -39,7 +39,7 @@
- if content_for?(:footer) - if content_for?(:footer)
= content_for(:footer) = content_for(:footer)
- if Flipflop.xray_enabled? - if feature_enabled?(:xray)
= javascript_include_tag :xray = javascript_include_tag :xray
= yield :charts_js = yield :charts_js

View file

@ -24,5 +24,5 @@ as defined by the routes in the `admin/` namespace
<hr /> <hr />
<%= link_to "Delayed Jobs", manager_delayed_job_path, class: "navigation__link" %> <%= link_to "Delayed Jobs", manager_delayed_job_path, class: "navigation__link" %>
<%= link_to "Features", manager_flipflop_path, class: "navigation__link" %> <%= link_to "Features", manager_flipper_path, class: "navigation__link" %>
</nav> </nav>

View file

@ -42,5 +42,6 @@ module TPS
config.ds_weekly_overview = ENV['APP_NAME'] == 'tps' config.ds_weekly_overview = ENV['APP_NAME'] == 'tps'
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
config.middleware.use Flipper::Middleware::Memoizer, preload_all: true
end end
end end

View file

@ -1,42 +0,0 @@
Flipflop.configure do
strategy :cookie,
secure: Rails.env.production?,
httponly: true
strategy :active_record
strategy :user_preference
strategy :default
group :champs do
feature :champ_integer_number,
title: "Champ nombre entier"
end
feature :web_hook
feature :operation_log_serialize_subject
feature :download_as_zip_enabled
feature :bypass_email_login_token,
default: Rails.env.test?
group :development do
feature :mini_profiler_enabled,
default: Rails.env.development?
feature :xray_enabled,
default: Rails.env.development?
end
group :production do
feature :insee_api_v3,
default: true
feature :pre_maintenance_mode
feature :maintenance_mode
end
if Rails.env.test?
# It would be nicer to configure this in administrateur_spec.rb in #feature_enabled?,
# but that results in a FrozenError: can't modify frozen Hash
feature :test_a
feature :test_b
end
end

View file

@ -0,0 +1,46 @@
Flipper.configure do |config|
config.default do
Flipper.new(Flipper::Adapters::ActiveRecord.new)
end
end
Flipper.register('Administrateurs') do |user|
user.administrateur_id.present?
end
Flipper.register('Instructeurs') do |user|
user.instructeur_id.present?
end
# This setup is primarily for first deployment, because consequently
# we can add new features from the Web UI. However when the new DB is created
# this will immediately migrate the default features to be controlled.
def setup_features(features)
features.each do |feature|
if Flipper.exist?(feature)
return
end
# Disable feature by default
Flipper.disable(feature)
end
end
# A list of features to be deployed on first push
features = [
:administrateur_champ_integer_number,
:administrateur_web_hook,
:insee_api_v3,
:instructeur_bypass_email_login_token,
:instructeur_download_as_zip,
:maintenance_mode,
:mini_profiler,
:operation_log_serialize_subject,
:pre_maintenance_mode,
:xray
]
ActiveSupport.on_load(:active_record) do
if ActiveRecord::Base.connection.data_source_exists? 'flipper_features'
setup_features(features)
end
end

View file

@ -21,17 +21,16 @@ Rails.application.routes.draw do
resources :administrateurs, only: [:index, :show, :new, :create] do resources :administrateurs, only: [:index, :show, :new, :create] do
post 'reinvite', on: :member post 'reinvite', on: :member
put 'enable_feature', on: :member
delete 'delete', on: :member delete 'delete', on: :member
end end
resources :users, only: [:index, :show] do resources :users, only: [:index, :show] do
post 'resend_confirmation_instructions', on: :member post 'resend_confirmation_instructions', on: :member
put 'enable_feature', on: :member
end end
resources :instructeurs, only: [:index, :show] do resources :instructeurs, only: [:index, :show] do
post 'reinvite', on: :member post 'reinvite', on: :member
put 'enable_feature', on: :member
end end
resources :dossiers, only: [:show] resources :dossiers, only: [:show]
@ -46,7 +45,7 @@ Rails.application.routes.draw do
post 'demandes/refuse_administrateur' post 'demandes/refuse_administrateur'
authenticate :administration do authenticate :administration do
mount Flipflop::Engine => "/features" mount Flipper::UI.app(-> { Flipper.instance }) => "/features", as: :flipper
match "/delayed_job" => DelayedJobWeb, :anchor => false, :via => [:get, :post] match "/delayed_job" => DelayedJobWeb, :anchor => false, :via => [:get, :post]
end end

View file

@ -0,0 +1,22 @@
class CreateFlipperTables < ActiveRecord::Migration[5.2]
def self.up
create_table :flipper_features do |t|
t.string :key, null: false
t.timestamps null: false
end
add_index :flipper_features, :key, unique: true
create_table :flipper_gates do |t|
t.string :feature_key, null: false
t.string :key, null: false
t.string :value
t.timestamps null: false
end
add_index :flipper_gates, [:feature_key, :key, :value], unique: true
end
def self.down
drop_table :flipper_gates
drop_table :flipper_features
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_08_22_143413) do ActiveRecord::Schema.define(version: 2019_08_28_073736) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -355,6 +355,22 @@ ActiveRecord::Schema.define(version: 2019_08_22_143413) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
create_table "flipper_features", force: :cascade do |t|
t.string "key", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["key"], name: "index_flipper_features_on_key", unique: true
end
create_table "flipper_gates", force: :cascade do |t|
t.string "feature_key", null: false
t.string "key", null: false
t.string "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["feature_key", "key", "value"], name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true
end
create_table "follows", id: :serial, force: :cascade do |t| create_table "follows", id: :serial, force: :cascade do |t|
t.integer "instructeur_id", null: false t.integer "instructeur_id", null: false
t.integer "dossier_id", null: false t.integer "dossier_id", null: false

View file

@ -0,0 +1,33 @@
namespace :after_party do
desc 'Deployment task: migrate_flipflop_to_flipper'
task migrate_flipflop_to_flipper: :environment do
puts "Running deploy task 'migrate_flipflop_to_flipper'"
Instructeur.includes(:user).find_each do |instructeur|
if instructeur.features['download_as_zip_enabled']
pp "enable :instructeur_download_as_zip for #{instructeur.user.email}"
Flipper.enable_actor(:instructeur_download_as_zip, instructeur.user)
end
if instructeur.features['bypass_email_login_token']
pp "enable :instructeur_bypass_email_login_token for #{instructeur.user.email}"
Flipper.enable_actor(:instructeur_bypass_email_login_token, instructeur.user)
end
end
Administrateur.includes(:user).find_each do |administrateur|
if administrateur.features['web_hook']
pp "enable :administrateur_web_hook for #{administrateur.user.email}"
Flipper.enable_actor(:administrateur_web_hook, administrateur.user)
end
if administrateur.features['champ_integer_number']
pp "enable :administrateur_champ_integer_number for #{administrateur.user.email}"
Flipper.enable_actor(:administrateur_champ_integer_number, administrateur.user)
end
end
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20190829065022'
end
end

View file

@ -167,7 +167,7 @@ describe ApplicationController, type: :controller do
let(:sensitive_path) { true } let(:sensitive_path) { true }
before do before do
Flipflop::FeatureSet.current.test!.switch!(:bypass_email_login_token, false) Flipper.disable(:instructeur_bypass_email_login_token)
end end
context 'when the instructeur is signed_in' do context 'when the instructeur is signed_in' do

View file

@ -544,10 +544,6 @@ describe Instructeurs::DossiersController, type: :controller do
end end
context 'when zip download is disabled through flipflop' do context 'when zip download is disabled through flipflop' do
before do
Flipflop::FeatureSet.current.test!.switch!(:download_as_zip_enabled, false)
end
it 'is forbidden' do it 'is forbidden' do
subject subject
expect(response).to have_http_status(:forbidden) expect(response).to have_http_status(:forbidden)

View file

@ -21,17 +21,6 @@ describe Administrateur, type: :model do
end end
end end
describe '#feature_enabled?' do
let(:administrateur) { create(:administrateur) }
before do
administrateur.enable_feature(:test_a)
end
it { expect(administrateur.feature_enabled?(:test_b)).to be_falsey }
it { expect(administrateur.feature_enabled?(:test_a)).to be_truthy }
end
# describe '#password_complexity' do # describe '#password_complexity' do
# let(:email) { 'mail@beta.gouv.fr' } # let(:email) { 'mail@beta.gouv.fr' }
# let(:passwords) { ['pass', '12pass23', 'démarches ', 'démarches-simple', 'démarches-simplifiées-pwd'] } # let(:passwords) { ['pass', '12pass23', 'démarches ', 'démarches-simple', 'démarches-simplifiées-pwd'] }

View file

@ -139,7 +139,7 @@ RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods config.include FactoryBot::Syntax::Methods
config.before(:each) do config.before(:each) do
Flipflop::FeatureSet.current.test!.reset! Flipper.enable(:instructeur_bypass_email_login_token)
end end
config.before(:all) { config.before(:all) {

View file

@ -22,7 +22,7 @@ module FeatureHelpers
fill_in :user_password, with: password fill_in :user_password, with: password
if sign_in_by_link if sign_in_by_link
Flipflop::FeatureSet.current.test!.switch!(:bypass_email_login_token, false) Flipper.disable(:instructeur_bypass_email_login_token)
end end
perform_enqueued_jobs do perform_enqueued_jobs do