divergence 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ log/*.log
19
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in divergence.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright 2012 LayerVault Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ryan LeFevre
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # Divergence
2
+
3
+ Map subdomains to git branches for switching live codebases on the fly. It's a Rack application that acts as a HTTP proxy between you and your web application for rapid testing.
4
+
5
+ ## Installation
6
+
7
+ First, you will need to install the gem:
8
+
9
+ ```
10
+ gem install divergence
11
+ ```
12
+
13
+ Then, since divergence is a rackup application, you will need to initialize it somewhere by running:
14
+
15
+ ```
16
+ divergence init
17
+ ```
18
+
19
+ This copies all of the necessary files into the current folder for you.
20
+
21
+ ## Config
22
+
23
+ All configuration happens in `config/config.rb`. You must set the git repository root and the application root before using divergence.
24
+
25
+ You will probably want divergence to take over port 80 on your testing server, so you may have to update the forwarding host/port. Note, this is the address where your actual web application can be reached.
26
+
27
+ ### Callbacks
28
+
29
+ Divergence lets you hook into various callbacks throughout the entire process. These are defined in `config/callbacks.rb`. Most callbacks automatically change the current working directory for you in order to make modifications as simple as possible.
30
+
31
+ The available callbacks are:
32
+
33
+ * before_swap
34
+ * Active dir: git repository
35
+ * after_swap
36
+ * Active dir: application
37
+ * before_pull
38
+ * Active dir: git repository
39
+ * Only executes if a git pull is required
40
+ * after_pull
41
+ * Active dir: git repository
42
+ * Only executes if the git pull succeeds
43
+ * on_pull_error
44
+ * Active dir: git repository
45
+ * Executes if there is a problem checking out and pulling a branch
46
+ * on_branch_discover
47
+ * Active dir: git repository
48
+ * Executes if the subdomain has a dash in the name. The subdomain name is passed to the callback in the options hash.
49
+ * If the callback returns nil, Divergence will try to auto-detect the branch name, otherwise it will use whatever you return.
50
+
51
+ There are also some built-in helper methods that are available inside callbacks. They are:
52
+
53
+ * bundle_install
54
+ * restart_passenger
55
+
56
+ ### Github Service Hook
57
+
58
+ You can automatically keep the currently active branch up to date by using a Github service hook. In your repository on Github, go to Admin -> Service Hooks -> WebHook URLs. Add the url:
59
+
60
+ ```
61
+ http://divergence.[your domain].com/update
62
+ ```
63
+
64
+ Now, whenever you push code to your repository, divergence will automatically know and will update accordingly.
65
+
66
+ ## Running
67
+
68
+ To start divergence, simply run in the divergence directory you initialized:
69
+
70
+ ```
71
+ divergence start
72
+ ```
73
+
74
+ This will start up divergence on port 9292 by default. If you'd like divergence to run on a different port, you can specify that as well:
75
+
76
+ ```
77
+ divergence start --port=88
78
+ ```
79
+
80
+ There is also a `--dev` flag that will run divergence in the foreground instead of daemonizing it.
81
+
82
+ ### Port 80
83
+
84
+ On many systems, running on port 80 requires special permissions. If you try starting divergence, but get the error `TCPServer Error: Permission denied - bind(2)`, then you will need to run divergence with sudo (or as root). If you use RVM to manage multiple Ruby versions, then you can use `rvmsudo` instead.
85
+
86
+ Make sure, if you're using Git over SSH, that you have your repository's host added to your known hosts file for the root user.
87
+
88
+ ### HTTPS
89
+
90
+ Divergence currently does not support HTTPS on its own; however, you can still use HTTPS in combination with a load balancer if you enable SSL termination.
91
+
92
+ ## TODO
93
+
94
+ * Handle multiple users at the same time
95
+ * Build-in HTTPS support
96
+
97
+ ## Contributing
98
+
99
+ 1. Fork it
100
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
101
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
102
+ 4. Push to the branch (`git push origin my-new-feature`)
103
+ 5. Create new Pull Request
104
+
105
+ ## Authors
106
+
107
+ * [Ryan LeFevre](http://meltingice.net) - Project Creator
108
+
109
+ ## License
110
+
111
+ Licensed under the Apache 2.0 License. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :docs do
5
+ `rdoc --main lib/divergence.rb lib`
6
+ end
7
+
8
+ namespace :test do
9
+ Rake::TestTask.new(:rack) do |t|
10
+ t.libs << "test"
11
+ t.pattern = "test/*_test.rb"
12
+ t.verbose = true
13
+ end
14
+ end
15
+
16
+ task :test do
17
+ Rake::Task["test:rack"].invoke
18
+ end
data/bin/divergence ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "thor"
4
+ require "thor/group"
5
+
6
+ module CLI
7
+ class Init < Thor::Group
8
+ include ::Thor::Actions
9
+
10
+ desc "Initializes a divergence application into the current directory"
11
+
12
+ def self.source_root
13
+ ::File.expand_path('../../generators/files', __FILE__)
14
+ end
15
+
16
+ def create_directories
17
+ empty_directory "config"
18
+ empty_directory "log"
19
+ end
20
+
21
+ def copy_templates
22
+ template "config.rb", "config/config.rb"
23
+ template "callbacks.rb", "config/callbacks.rb"
24
+ template "config.ru", "config.ru"
25
+ end
26
+ end
27
+ end
28
+
29
+ module CLI
30
+ class Base < Thor
31
+ register CLI::Init, "init", "init", "Initializes a divergence application into the current directory"
32
+
33
+ desc "start", "Start divergence"
34
+ method_options :port => :number, :dev => :boolean
35
+ def start
36
+ cmd = 'rackup'
37
+ cmd << " -p #{options[:port]}" if options[:port]
38
+ cmd << " -D" unless options[:dev]
39
+
40
+ exec cmd
41
+ end
42
+ end
43
+ end
44
+
45
+ CLI::Base.start
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'divergence/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "divergence"
8
+ gem.version = Divergence::VERSION
9
+ gem.authors = ["Ryan LeFevre"]
10
+ gem.email = ["ryan@layervault.com"]
11
+ gem.description = "Map subdomains to git branches for switching live codebases on the fly. It's a Rack application that acts as a HTTP proxy between you and your web application for rapid testing."
12
+ gem.summary = "Map virtual host subdomains to git branches for testing"
13
+ gem.homepage = "http://cosmos.layervault.com/divergence.html"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "rack"
21
+ gem.add_dependency "rack-proxy"
22
+ gem.add_dependency "thor"
23
+ gem.add_dependency "git"
24
+ gem.add_dependency "json"
25
+ gem.add_development_dependency "rack-test"
26
+ end
@@ -0,0 +1,9 @@
1
+ Divergence::Application.configure do |config|
2
+ config.callbacks :after_swap do
3
+ # Run anything after the swap finishes
4
+ #
5
+ # after_swap changes to the app directory for you, so
6
+ # you can simply run any commands you want. There are
7
+ # some built-in helpers too.
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ require File.expand_path('../callbacks', __FILE__)
2
+
3
+ Divergence::Application.configure do |config|
4
+ config.git_path = nil # Change this to the git repository path
5
+ config.app_path = nil # and this to your application's path.
6
+
7
+ # Where should we proxy this request to? Normally you can leave
8
+ # the host as 'localhost', but if you are using virtual hosts in
9
+ # your web server setup, you may need to be more specific. You
10
+ # will probably want Divergence to take over port 80 as well,
11
+ # so update your web application to run on a different port.
12
+ config.forward_host = 'localhost'
13
+ config.forward_port = 80
14
+ end
@@ -0,0 +1,4 @@
1
+ require 'divergence'
2
+ require ::File.expand_path('../config/config', __FILE__)
3
+
4
+ run Divergence::Application.new()
@@ -0,0 +1,58 @@
1
+ module Divergence
2
+ class Configuration
3
+ include Enumerable
4
+
5
+ attr_accessor :app_path, :git_path
6
+ attr_accessor :forward_host, :forward_port
7
+
8
+ def initialize
9
+ @git_path = nil
10
+ @app_path = nil
11
+ @forward_host = 'localhost'
12
+ @forward_port = 80
13
+
14
+ @callback_store = {}
15
+ @helpers = Divergence::Helpers.new(self)
16
+ end
17
+
18
+ # Might get rid of realpath in the future because it
19
+ # resolves symlinks and that could be problematic
20
+ # with capistrano in case someone accidentally deploys.
21
+ def app_path=(p)
22
+ @app_path = File.realpath(p)
23
+ end
24
+
25
+ def git_path=(p)
26
+ @git_path = File.realpath(p)
27
+ end
28
+
29
+ # Lets a user define a callback for a specific event
30
+ def callbacks(name, &block)
31
+ unless @callback_store.has_key?(name)
32
+ @callback_store[name] = []
33
+ end
34
+
35
+ @callback_store[name].push block
36
+ end
37
+
38
+ def callback(name, args = {})
39
+ return unless @callback_store.has_key?(name)
40
+
41
+ Application.log.debug "Execute callback: #{name.to_s}"
42
+
43
+ @callback_store[name].each do |cb|
44
+ @helpers.execute cb, args
45
+ end
46
+ end
47
+
48
+ def each(&block)
49
+ instance_variables.each do |key|
50
+ if block_given?
51
+ block.call key, instance_variable_get(key)
52
+ else
53
+ yield instance_variable_get(key)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,150 @@
1
+ module Divergence
2
+ class GitManager
3
+ attr_reader :current_branch
4
+
5
+ def initialize(config)
6
+ @config = config
7
+ @app_path = config.app_path
8
+ @git_path = config.git_path
9
+
10
+ @log = Logger.new('./log/git.log')
11
+ @git = Git.open(@git_path, :log => @log)
12
+
13
+ @current_branch = @git.branch
14
+ @new_branch = false
15
+ end
16
+
17
+ def prepare_directory(branch, force=false)
18
+ return if is_current?(branch) and !force
19
+ pull branch
20
+ end
21
+
22
+ # Performs the swap between the git directory and the working
23
+ # app directory. We want to copy the files without copying
24
+ # the .git directory, but this is a temporary dumb solution.
25
+ #
26
+ # Future idea: try the capistrano route and simply symlink
27
+ # to the git directory instead of copying files.
28
+ #
29
+ # TODO: make this more ruby-like.
30
+ def swap!
31
+ return unless @new_branch
32
+
33
+ Dir.chdir @config.git_path do
34
+ @config.callback :before_swap
35
+ end
36
+
37
+ Application.log.info "Swap: #{@git_path} -> #{@app_path}"
38
+ `rsync -a --delete --exclude=.git #{@git_path}/* #{@app_path}`
39
+ @new_branch = false
40
+
41
+ Dir.chdir @config.app_path do
42
+ @config.callback :after_swap
43
+ end
44
+ end
45
+
46
+ # Since underscores are technically not allowed in URLs,
47
+ # but they are allowed in Git branch names, we have to do
48
+ # some magic to possibly convert dashes to underscores
49
+ # so we can load the right branch.
50
+ #
51
+ # Another possible thing to explore is converting all
52
+ # dashes in the URL to a regex search against all branches
53
+ # in this repository to avoid the current brute-force
54
+ # solution we're using.
55
+ def discover(branch)
56
+ return branch if is_branch?(branch)
57
+
58
+ Dir.chdir @git_path do
59
+ resp = @config.callback :on_branch_discover, branch
60
+
61
+ unless resp.nil?
62
+ return resp
63
+ end
64
+
65
+ local_search = "^" + branch.gsub(/-/, ".") + "$"
66
+ remote_search = "^remotes/origin/(" + branch.gsub(/-/, ".") + ")$"
67
+ local_r = Regexp.new(local_search, Regexp::IGNORECASE)
68
+ remote_r = Regexp.new(remote_search, Regexp::IGNORECASE)
69
+
70
+ `git branch -a`.split("\n").each do |b|
71
+ b = b.gsub('*', '').strip
72
+
73
+ return b if local_r.match(b)
74
+ if remote_r.match(b)
75
+ return remote_r.match(b)[1]
76
+ end
77
+ end
78
+ end
79
+
80
+ raise "Unable to automatically detect branch. Given = #{branch}"
81
+ end
82
+
83
+ def is_current?(branch)
84
+ @current_branch.to_s == branch
85
+ end
86
+
87
+ private
88
+
89
+ def is_branch?(branch)
90
+ Dir.chdir @git_path do
91
+ # This is fast, but only works on locally checked out branches
92
+ `git show-ref --verify --quiet 'refs/heads/#{branch}'`
93
+ return true if $?.exitstatus == 0
94
+
95
+ # This is slow and will only get called for remote branches.
96
+ result = `git ls-remote --heads origin 'refs/heads/#{branch}'`
97
+ return result.strip.length != 0
98
+ end
99
+ end
100
+
101
+ def pull(branch)
102
+ if checkout(branch)
103
+ Dir.chdir @config.git_path do
104
+ @config.callback :before_pull
105
+ end
106
+
107
+ #@git.pull 'origin', branch
108
+ @git.chdir do
109
+ # For some reason, I'm having issues with the pull
110
+ # that's built into the library. Doing this manually
111
+ # for now.
112
+ @log.info "git pull origin #{branch} 2>&1"
113
+ `git pull origin #{branch} 2>&1`
114
+ end
115
+
116
+ Dir.chdir @config.git_path do
117
+ @config.callback :after_pull
118
+ end
119
+ else
120
+ Dir.chdir @config.git_path do
121
+ @config.callback :on_pull_error
122
+ end
123
+
124
+ return false
125
+ end
126
+ end
127
+
128
+ def checkout(branch)
129
+ fetch
130
+ reset
131
+
132
+ begin
133
+ @git.checkout branch, :force => true
134
+ @current_branch = branch
135
+ @new_branch = true
136
+ rescue
137
+ return false
138
+ end
139
+ end
140
+
141
+ def reset
142
+ @git.reset_hard('HEAD')
143
+ end
144
+
145
+ # Fetch all remote branch information
146
+ def fetch
147
+ @git.fetch
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,34 @@
1
+ require 'fileutils'
2
+
3
+ module Divergence
4
+ class Helpers
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ def execute(block, opts={})
10
+ self.instance_exec opts, &block
11
+ end
12
+
13
+ private
14
+
15
+ def bundle_install
16
+ Application.log.debug "bundle install"
17
+
18
+ Dir.chdir @config.app_path do
19
+ `bundle install`
20
+ end
21
+ end
22
+
23
+ def restart_passenger
24
+ Application.log.debug "Restarting passenger..."
25
+
26
+ Dir.chdir @config.app_path do
27
+ begin
28
+ FileUtils.touch 'tmp/restart.txt'
29
+ rescue
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,52 @@
1
+ module Divergence
2
+ class RequestParser
3
+ def initialize(env, git)
4
+ @req = Rack::Request.new(env)
5
+ @git = git
6
+ end
7
+
8
+ def raw
9
+ @req
10
+ end
11
+
12
+ def is_webhook?
13
+ subdomain == "divergence" and
14
+ @req.env['PATH_INFO'] == "/update" and
15
+ @req.post?
16
+ end
17
+
18
+ def host_parts
19
+ @req.host.split(".")
20
+ end
21
+
22
+ def has_subdomain?
23
+ host_parts.length > 2
24
+ end
25
+
26
+ def subdomain
27
+ if has_subdomain?
28
+ host_parts.shift
29
+ else
30
+ nil
31
+ end
32
+ end
33
+
34
+ def branch
35
+ if has_subdomain?
36
+ branch = subdomain
37
+
38
+ if branch['-']
39
+ @git.discover(branch)
40
+ else
41
+ branch
42
+ end
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+ def method_missing(meth, *args, &block)
49
+ raw.send(meth, *args)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,71 @@
1
+ module Divergence
2
+ class Application < Rack::Proxy
3
+ # The main entry point for the application. This is caled
4
+ # by Rack.
5
+ def call(env)
6
+ @req = RequestParser.new(env, @g)
7
+
8
+ # First, lets find out what subdomain/git branch
9
+ # we're dealing with (if any).
10
+ unless @req.has_subdomain?
11
+ # No subdomain, simply proxy the request.
12
+ return proxy(env)
13
+ end
14
+
15
+ # Handle webhooks from Github for updating the current
16
+ # branch if necessary.
17
+ if @req.is_webhook?
18
+ return Webhook.handle @g, @req
19
+ end
20
+
21
+ # Ask our GitManager to prepare the directory
22
+ # for the given branch.
23
+ result = @g.prepare_directory @req.branch
24
+ if result === false
25
+ return error!
26
+ end
27
+
28
+ # And then perform the codebase swap
29
+ @g.swap!
30
+
31
+ # We're finished, pass the request through.
32
+ proxy(env)
33
+ end
34
+
35
+ private
36
+
37
+ def proxy(env)
38
+ fix_environment!(env)
39
+
40
+ status, header, body = perform_request(env)
41
+
42
+ # This is super weird. Not sure why there is a status
43
+ # header coming through, but Rack::Lint complains about
44
+ # it, so we just remove it. I think this might be coming
45
+ # from Cloudfront (if you use it).
46
+ if header.has_key?('Status')
47
+ header.delete 'Status'
48
+ end
49
+
50
+ [status, header, body]
51
+ end
52
+
53
+ # Sets the forwarding host for the request. This is where
54
+ # the proxy comes in.
55
+ def fix_environment!(env)
56
+ env["HTTP_HOST"] = "#{config.forward_host}:#{config.forward_port}"
57
+ end
58
+
59
+ def error!
60
+ Application.log.error "Branch #{@req.branch} does not exist"
61
+ Application.log.error @req.raw
62
+
63
+ public_path = File.expand_path('../../../public', __FILE__)
64
+ file = File.open("#{public_path}/404.html", "r")
65
+ contents = file.read
66
+ file.close
67
+
68
+ [404, {"Content-Type" => "text/html"}, [contents]]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,3 @@
1
+ module Divergence
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ module Divergence
2
+ class Webhook
3
+ def self.handle(git, req)
4
+ hook = JSON.parse(req['payload'])
5
+ branch = hook["ref"].split("/").last.strip
6
+
7
+ Application.log.info "Webhook: received for #{branch} branch"
8
+
9
+ # If the webhook is for the currently active branch,
10
+ # then we perform a pull and a swap.
11
+ if git.is_current?(branch)
12
+ Application.log.info "Webhook: updating #{branch}"
13
+
14
+ git.prepare_directory(branch, true)
15
+ git.swap!
16
+
17
+ ok
18
+ else
19
+ ignore
20
+ end
21
+ end
22
+
23
+ def self.ok
24
+ [200, {"Content-Type" => "text/html"}, ["OK"]]
25
+ end
26
+
27
+ def self.ignore
28
+ [200, {"Content-Type" => "text/html"}, ["IGNORE"]]
29
+ end
30
+ end
31
+ end