2

tl;dr我怎样才能让一个 Sinatra 应用程序通过自定义在不同的服务器上以非常不同的方式启动config.ru

背景

我有一个使用 Sinatra 编写的 Web 应用程序,它在不同的服务器上运行。目前,这些服务器的代码库是分叉的,因为它们的工作方式(离散)部分存在一些重大差异。例如:

  • 一台服务器通过 Intranet LDAP 服务器对用户进行身份验证,而另一台服务器使用更简单的本地数据库表查找。
  • 一台服务器使用外部 cron 作业定期更新一些统计信息,而另一台(基于 Windows 的)服务器使用内部休眠线程。
  • 一台服务器将某些元数据存储在本地表中,而另一台服务器通过屏幕抓取 (!) 从外部 Wiki 提取元数据。

…等等。

我想让这些代码库完全共享(单个 Git 存储库)。我设想每台服务器都会有一个稍微不同的配置文件,这会导致应用程序以不同的方式启动。

废弃的解决方案

可以根据环境变量更改应用程序的行为。由于行为的变化并不多,我宁愿不隐藏环境变量中的设置。

可以创建自己的“server-settings.rb”文件,该文件对每台机器都是唯一的,在我app.rb的 . 然而,这似乎是在重新发明轮子。我已经config.ru每个服务器命名了一个文件。我不应该用这个吗?

当前代码

config.ru目前的应用程序很简单:

require ::File.join( ::File.dirname(__FILE__), 'app' )
run MyApp.new

app.rb它需要的本质上是:

require 'sinatra'
require_relative 'helpers/login' # customized for LDAP lookup on this server

class MyApp < Sinatra::Application
  use Rack::Session::Cookie, key:'foo.bar', path:'/', secret:'ohnoes'
  set :protection, except: [:path_traversal, :session_hijacking]
  configure :production do
    # run various code that depends on server settings, e.g.
    Snapshotter.start # there is no cron on this machine, so we do it ourselves
  end
  configure :development do
    # run various code that depends on server settings
  end
end

问题

我想不config.ru辜负它的名字,让它看起来像这样:

require ::File.join( ::File.dirname(__FILE__), 'app' )
run MyApp.new( auth: :ldap, snapshot:false, metadata: :remote_wiki, … )

如何根据通过提供的设置修改我的应用程序以更改其配置行为config.ru?或者这是对 的滥用config.ru,试图将其用于完全错误的事情?

4

1 回答 1

2

当我开始阅读这个问题时,我脑海中出现的第一个答案是“环境变量”,但你马上就忽略了 :)

我将混合使用您的一个罐头和所需的结果代码,因为这是我构建事物的方式……</p>

因为我希望能够更轻松地测试我的应用程序,所以我将大部分 Ruby 从 config.ru 中取出并放入一个单独的config.rb文件中,并将 config.ru 保留为引导文件。所以我的标准骨架是:

配置.ru

# encoding: UTF-8

require 'rubygems'
require 'bundler'
Bundler.setup

root = File.expand_path File.dirname(__FILE__)
require File.join( root , "./app/config.rb" )

# everything was moved into a separate module/file to make it easier to set up tests

map "/" do
  run APP_NAME.app
end

应用程序/config.rb

# encoding: utf-8
require_relative File.expand_path(File.join File.dirname(__FILE__), "../lib/ext/warn.rb")

require_relative "./init.rb"  # config
require_relative "./main.rb"  # routes and helpers
require 'encrypted_cookie'

# standard cookie settings
COOKIE_SETTINGS = {
  :key => 'usr',
  :path => "/",
  :expire_after => 86400, # In seconds, 1 day
  :secret => ENV["LLAVE"],
  :httponly => true
}

module APP_NAME # overall name of the app

  require 'rack/ssl' # force SSL
  require 'rack/csrf'

  if ENV["RACK_ENV"] == "development"
    require 'pry'
    require 'pry-nav'
  end

  # from http://devcenter.heroku.com/articles/ruby#logging
  $stdout.sync = true

  ONE_MONTH = 60 * 60 * 24 * 30

  def self.app
    Rack::Builder.app do

      cookie_settings = COOKIE_SETTINGS
      # more security if in production
      cookie_settings.merge!( :secure => true ) if ENV["RACK_ENV"] == "production"

      # AES encryption of cookies
      use Rack::Session::EncryptedCookie, cookie_settings

      if ENV["RACK_ENV"] == "production"
        use Rack::SSL, :hsts => {:expires => ONE_MONTH}
      end

      # to stop XSS
      use Rack::Csrf, :raise => true unless ENV["RACK_ENV"] == "test"

      run App # the main Sinatra app
    end
  end # self.app

end # APP_NAME

我这样做的最初原因是让在规范中运行应用程序变得容易:

shared_context "All routes" do
  include Rack::Test::Methods
  let(:app){ APP_NAME.app }
end

但对我来说,将此代码与应用程序代码的其余部分一起保存是有意义的,可以这么说,因为我可以将东西捆绑在一起,运行其他应用程序等。我已经使用它有条件地将不同的示例加载到规范中项目(它有助于减少重复工作并检查示例是否真的有效),所以我不明白为什么你不能使用它来有条件地加载配置。

这样,您可以选择在 config.ru 中使用条件来确定要使用的 config.rb 文件,或者在 config.rb 中使用 env var 来确定self.app要使用的定义,或者传入选项哈希到 self.app…</p>

通过您的设置,我将APP_NAME模块重命名为MyApp,并将 Sinatra 类重命名为App(因为我经常会有一个运行前端和 API 的网站,因此 Sinatra 类由它们的功能(应用程序、API 等)命名) 并包装在以该站点命名的模块中) 并最终得到:

配置.ru

map "/" do
  run MyApp.app( auth: :ldap, snapshot:false, metadata: :remote_wiki )
end

配置文件

def self.app( opts={} )
  opts = DEFAULT_OPTIONS.merge opts
  # …
  run App
end

看看其他人如何解决这个问题会很有趣。

于 2013-02-25T09:25:07.583 回答