overlay 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af4c5cc9e0f3df5433871de6e021c072fc3e5122
4
- data.tar.gz: 5a769dbe4d754ec40db970a19bd2b536299d30c6
3
+ metadata.gz: 9e7cf4853cb51c3c998a8b0d89691dd7fe769b71
4
+ data.tar.gz: a3a51bcfb29b2d56ebd16b25547c609476506afd
5
5
  SHA512:
6
- metadata.gz: 9566bcd6762c5035425aa5467fc1e5151366793335ad31c2de3668c5893f93f3e85d10b5a8a08d2924688c9e6c6660a8b4b38da490b338706ef1703bef328aab
7
- data.tar.gz: b1981b111c43b6bab9b650ce86a07fab40fc77d83dc669982fda21f6be5ddecbd1ae67f79d1f8b25002a0d3a144cdf060194a468a36014eda30fa33facdade74
6
+ metadata.gz: 5dae53b5bd7073acba446d4a605341b473139bef5e763582946bf9173251474e29ce523cd65bb9e6e6cc074c6a58b0e966c8bc093ee44d4892b8717172ca43e6
7
+ data.tar.gz: 4e7f06ee20f890721b59bd548b1e1ffee3d9b75da166206518314ac52563157d94750878e1b1f8e0f043f7202129f711da8a86591c6a1dcb3e68a63cf5a6e0de
data/README.rdoc CHANGED
@@ -1,15 +1,55 @@
1
- # Overlay
1
+ Overlay
2
+ ====================
2
3
 
3
- Overlay a repository on to an existing Rails application.
4
+ Rails engine that allows for overlaying external templates onto an existing Rails application. Overlayed directories are prepended to the view path to allow overwriting of deployed templates.
4
5
 
5
- ## Featrures
6
+ Features
7
+ ====================
6
8
 
7
- ## Installation
9
+ GithubRepo Features
10
+ ---------------------
8
11
 
9
- ## Configuration
12
+ * Overlay separate directories in a single repo to specific places in your Rails application.
13
+ * Update files in realtime utilizing self registering post commit webhooks on github.
14
+ * Run code on file update via the GithubRepo #after_process_hook block.
15
+ * Utilize an OverlayPublisher application and a redis server to centralize hook management and publish changes to a fleet of servers.
10
16
 
11
- ## Usage
17
+ Installation
18
+ ====================
12
19
 
13
- ## Examples
20
+ Add the gem to your Gemfile:
21
+
22
+ gem 'overlay'
23
+
24
+ Configuration
25
+ ====================
26
+
27
+ Add an initializer to your Rails `config/initializers` directory. This file should configure your repositories and launch the initial overlay. Here is a sample initializer:
28
+
29
+ require 'overlay'
30
+
31
+ Overlay.configure do |config|
32
+ config.relative_root_url = Rails.application.config.relative_url_root
33
+
34
+ github_repo = Overlay::GithubRepo.new(
35
+ 'repo_org',
36
+ 'repo_name',
37
+ 'repo_user_user:repo_password',
38
+ 'source_root_directory',
39
+ 'source_destination_path'
40
+ )
41
+ config.repositories << repo_config
42
+ end
43
+
44
+ # Overlay files after rails is initialized
45
+ #
46
+ Rails.application.config.after_initialize do
47
+ Overlay::Github.instance.process_overlays
48
+ end
49
+
50
+ Usage
51
+ ====================
52
+
53
+ Once Overlay is configured, on startup, a process will be forked to run the initial pull-down of files from the repository. Overlay will update specific files on change in the repo through use of Github webhooks.
14
54
 
15
55
  This project rocks and uses MIT-LICENSE.
@@ -1,18 +1,15 @@
1
- require_dependency "overlay/application_controller"
2
-
3
1
  module Overlay
4
- class GithubController < ApplicationController
2
+ class GithubController < Overlay::ApplicationController
5
3
  def update
6
4
  render nothing: true
7
5
 
8
6
  Overlay.configuration.repositories.each do |repo_config|
9
7
  next unless repo_config.class == GithubRepo
10
- branch = repo_config[:branch] || 'master'
8
+ branch = repo_config.branch
11
9
 
12
10
  if (params[:repository] && params[:ref])
13
- if (params[:repository][:name] == repo_config[:repo]) && (params[:ref] == "refs/heads/#{branch}")
14
- logger.info "Enqueueing GithubJob for repo: #{repo_config[:repo]} and branch: #{repo_config[:branch] || 'master'}"
15
- Overlay::Github.overlay_repo repo_config
11
+ if (params[:repository][:name] == repo_config.repo) && (params[:ref] == "refs/heads/#{branch}")
12
+ Overlay::Github.instance.process_hook(params, repo_config)
16
13
  end
17
14
  end
18
15
  end
@@ -1,16 +1,15 @@
1
+
1
2
  module Overlay
2
3
  VALID_OPTIONS_KEYS = [
3
- :site,
4
- :endpoint,
5
- :repo,
6
- :user,
7
- :auth,
8
4
  :repositories,
9
5
  :host_name,
10
6
  :host_port,
11
7
  :relative_root_url
12
8
  ].freeze
13
9
 
10
+ # Exceptions
11
+ class RequiredParameterError < StandardError; end
12
+
14
13
  class << self
15
14
  attr_accessor :configuration
16
15
  end
@@ -28,11 +27,106 @@ module Overlay
28
27
  end
29
28
 
30
29
  def reset
31
- @site = 'https://github.com'
32
- @endpoint = 'https://api.github.com'
33
- @repositories = Set.new
30
+ @repositories = Set.new
34
31
  end
35
32
  end
36
33
 
37
- GithubRepo = Struct.new(:user, :repo, :branch, :root_source_path, :root_dest_path)
34
+ # Configure a github repository. Required parameters:
35
+ # :org,
36
+ # :repo,
37
+ # :auth,
38
+ # :root_source_path,
39
+ # :root_dest_path
40
+
41
+ # Optional parameters:
42
+ # :branch,
43
+ # :use_publisher
44
+ # :registration_address
45
+ # :redis_server
46
+ # :redis_port
47
+ # :endpoint,
48
+ # :site,
49
+ class GithubRepo
50
+ attr_accessor :root_source_path, :root_dest_path, :branch
51
+ attr_accessor :use_publisher, :redis_server, :redis_port, :registration_server
52
+ attr_reader :repo, :org, :auth, :endpoint, :site
53
+
54
+ # Internal repo api hook
55
+ attr_accessor :github_api
56
+
57
+ REQUIRED_PARAMS = [:org, :repo, :auth, :root_source_path]
58
+ REQUIRED_PUBLISHER_PARAMS = [:redis_server, :redis_port, :registration_server]
59
+
60
+ def initialize(org, repo, auth, root_source_path, root_dest_path)
61
+ @org = org
62
+ @repo = repo
63
+ @auth = auth
64
+ @root_source_path = root_source_path
65
+ @root_dest_path = root_dest_path
66
+ @branch = 'master'
67
+ @use_publisher = false
68
+
69
+ # Quick sanity check
70
+ validate
71
+
72
+ # Create a hook to the Github API
73
+ initialize_api
74
+ end
75
+
76
+ def endpoint=(endpoint_addr)
77
+ @endpoint = endpoint_addr
78
+
79
+ # re-initialize api
80
+ initialize_api
81
+ end
82
+
83
+ def site=(site_addr)
84
+ @site = site_addr
85
+
86
+ # re-initialize api
87
+ initialize_api
88
+ end
89
+
90
+ # Make sure that this configuration has all required parameters
91
+ def validate
92
+ REQUIRED_PARAMS.each do |param|
93
+ raise RequiredParameterError, "Overlay GithubRepo missing required paramater: #{param}" if (send(param).nil? || send(param).empty?)
94
+ end
95
+
96
+ # If we are using a publisher, check required publisher params
97
+ if @use_publisher
98
+ REQUIRED_PUBLISHER_PARAMS.each do |param|
99
+ raise RequiredParameterError, "Overlay GithubRepo missing required paramater: #{param}" if (send(param).nil?)
100
+ end
101
+ end
102
+ end
103
+
104
+ # Add code to be called after processing a hook
105
+ def after_process_hook(&hook)
106
+ raise ArgumentError, "No block given" unless block_given?
107
+ @post_hook = hook
108
+ end
109
+
110
+ # Run post_hook code
111
+ def post_hook
112
+ @post_hook.call unless @post_hook.nil?
113
+ end
114
+
115
+ # Retrieve API hook to repo
116
+ def initialize_api
117
+ ::Github.reset!
118
+ ::Github.configure do |github_config|
119
+ github_config.endpoint = @endpoint if @endpoint
120
+ github_config.site = @site if @site
121
+ github_config.basic_auth = @auth
122
+ github_config.repo = @repo
123
+ github_config.org = @org
124
+ github_config.adapter = :net_http
125
+ github_config.ssl = {:verify => false}
126
+ end
127
+
128
+ @github_api = ::Github::Repos.new
129
+ end
130
+
131
+ end
38
132
  end
@@ -6,7 +6,7 @@ module Overlay
6
6
  ActionController::Base.class_eval do
7
7
  before_filter do
8
8
  Overlay.configuration.repositories.each do |repo_config|
9
- prepend_view_path repo_config[:root_dest_path]
9
+ prepend_view_path repo_config.root_dest_path
10
10
  end
11
11
  end
12
12
  end
@@ -1,40 +1,104 @@
1
1
  require 'github_api'
2
2
  require 'fileutils'
3
3
  require 'socket'
4
-
4
+ require 'singleton'
5
+ require 'redis'
6
+
7
+ # The github class is responsible for managing overlaying
8
+ # directories in a Github repo on the current application.
9
+ # Call to action is based on either a webhook call to the github controller
10
+ # or a publish event to a redis key.
11
+ #
5
12
  module Overlay
6
13
  class Github
14
+ include Singleton
7
15
  include Overlay::Engine.routes.url_helpers
16
+
17
+ attr_accessor :subscribed_configs
18
+
19
+ def initialize
20
+ @subscribed_configs = []
21
+ end
22
+
8
23
  # Cycle through all configured repositories and overlay
9
- #
10
- def self.process_overlays
24
+ # This function should be called only at initialization time as it causes a
25
+ # full overlay to be run
26
+ def process_overlays
11
27
  # This can be called in an application initializer which will
12
28
  # load anytime the environment is loaded. Make sure we are prepared to run
13
29
  # this.
14
30
  #
15
31
  return unless (config.host_port || ENV['SERVER_HOST_PORT'] || defined? Rails::Server)
16
32
 
17
- # Configure github api
18
- configure
19
-
20
33
  Overlay.configuration.repositories.each do |repo_config|
21
34
  next unless repo_config.class == GithubRepo
22
35
 
23
- # Validate repository config
24
- raise 'Respository config missing user' if (!repo_config[:user] || repo_config[:user].nil?)
25
- raise 'Respository config missing repo' if (!repo_config[:repo] || repo_config[:repo].nil?)
36
+ # Register this server's endpoint as a webhook or subscribe
37
+ # to a redis pub/sub
38
+ if repo_config.use_publisher
39
+ publisher_subscribe(repo_config)
40
+ else
41
+ register_web_hook(repo_config)
42
+ end
26
43
 
27
- repo_config[:branch] ||= 'master'
44
+ # Now that all the set-up is done, for a process
45
+ # to overlay the repo.
46
+ fork_it(:overlay_repo, repo_config)
47
+ end
48
+ end
28
49
 
29
- register_web_hook(repo_config)
50
+ # Take a hook hash and process each changelist.
51
+ # For every file updated, clone it back to us.
52
+ def process_hook hook, repo_config
53
+ # Grab the commit array
54
+ commits = hook['commits']
55
+
56
+ # We don't care if there aren't commits
57
+ return if commits.nil?
58
+
59
+ commits.each do |commit|
60
+ # There will be three entries in each commit with file paths: added, removed, and modified.
61
+ added_files = commit['added']
62
+ added_files.each do |file|
63
+ # Do we care?
64
+ if my_file?(file, repo_config)
65
+ Rails.logger.info "Overlay found added file in hook: #{file}"
66
+
67
+ # Make sure that the directory is in place
68
+ FileUtils.mkdir_p(destination_path(File.dirname(file), repo_config))
69
+ clone_file(file, repo_config)
70
+ end
71
+ end
72
+
73
+ modified_files = commit['added']
74
+ modified_files.each do |file|
75
+ # Do we care?
76
+ if my_file?(file, repo_config)
77
+ Rails.logger.info "Overlay found modified file in hook: #{file}"
78
+ clone_file(file, repo_config)
79
+ end
80
+ end
30
81
 
31
- overlay_repo repo_config
82
+ removed_files = commit['added']
83
+ removed_files.each do |file|
84
+ # Do we care?
85
+ if my_file?(file, repo_config)
86
+ Rails.logger.info "Overlay found deleted file in hook: #{file}"
87
+ File.delete(destination_path(file, repo_config))
88
+ end
89
+ end
32
90
  end
91
+
92
+ # Call post hook code
93
+ repo_config.post_hook
33
94
  end
34
95
 
96
+ private
97
+
35
98
  # Register our listener on the repo
36
99
  #
37
- def self.register_web_hook repo_config
100
+ def register_web_hook(repo_config)
101
+ Rails.logger.info "Overlay register webhook for repo: org => #{repo_config.org}, repo => #{repo_config.repo}"
38
102
  # Make sure our routes are loaded
39
103
  Rails.application.reload_routes!
40
104
 
@@ -44,46 +108,78 @@ module Overlay
44
108
  path = Overlay::Engine.routes.url_for({:controller=>"overlay/github", :action=>"update", :only_path => true})
45
109
  uri = ActionDispatch::Http::URL::url_for({:host => host, :port => port, :path => "#{config.relative_root_url}#{path}"})
46
110
 
111
+ github_api = repo_config.github_api
112
+
47
113
  # Retrieve current web hooks
48
- current_hooks = github_repo.hooks.list(repo_config[:user], repo_config[:repo]).response.body
114
+ current_hooks = github_api.hooks.list(repo_config.org, repo_config.repo).response.body
49
115
  if current_hooks.find {|hook| hook.config.url == uri}.nil?
50
116
  # register hook
51
- github_repo.hooks.create(repo_config[:user], repo_config[:repo], name: 'web', active: true, config: {:url => uri, :content_type => 'json'})
117
+ github_api.hooks.create(repo_config.org, repo_config.repo, name: 'web', active: true, config: {:url => uri, :content_type => 'json'})
52
118
  end
53
119
  end
54
120
 
55
- def self.overlay_repo repo_config
56
- Thread.new do
57
- Rails.logger.info "Start processing repo with config #{repo_config.inspect}"
58
- # Get our root entries
59
- #
60
- root = repo_config[:root_source_path] || '/'
61
-
62
- # If we have a root defined, jump right into it
63
- #
64
- if (root != '/')
65
- overlay_directory(root, repo_config)
66
- else
67
- root_entries = github_repo.contents.get(repo_config[:user], repo_config[:repo], root, ref: repo_config[:branch]).response.body
68
-
69
- # We aren't pulling anything out of root. Cycle through directories and overlay
70
- #
71
- root_entries.each do |entry|
72
- if entry.type == 'dir'
73
- overlay_directory(entry.path, repo_config)
74
- end
121
+ # Retrieve Subscribe to a OverlayPublisher redis key
122
+ # Fork a process that subscribes to the redis key and processes updates.
123
+ def publisher_subscribe repo_config
124
+ return unless @subscribed_configs.find_index(repo_config).nil?
125
+
126
+ # Validate our settings
127
+ repo_config.validate
128
+
129
+ # Register this repo with the manager
130
+ uri = URI.parse(repo_config.registration_server)
131
+ http = Net::HTTP.new(uri.host, uri.port)
132
+ request = Net::HTTP::Post.new("/register")
133
+ request.add_field('Content-Type', 'application/json')
134
+ request.body = {
135
+ 'organization' => repo_config.org,
136
+ 'repo' => repo_config.repo,
137
+ 'auth' => repo_config.auth,
138
+ 'endpoint' => repo_config.endpoint,
139
+ 'site' => repo_config.site
140
+ }.to_json
141
+
142
+ response = http.request(request)
143
+
144
+ # Retrieve publish key
145
+ publish_key = JSON.parse(response.read_body)['publish_key']
146
+
147
+ Rails.logger.info "Overlay subscribing to redis channel: #{publish_key}"
148
+
149
+ # Subscribe to redis channel
150
+ fork_it(:subscribe_to_channel, publish_key, repo_config)
151
+
152
+ @subscribed_configs << repo_config
153
+ end
154
+
155
+ # Overlay all the files specifiec by the repo_config. This
156
+ # process can be long_running so we fork. We should only be running
157
+ # this method in initialization of the application.
158
+ def overlay_repo repo_config
159
+ Rails.logger.info "Overlay started processing repo with config #{repo_config.inspect}"
160
+
161
+ # Get our root entries
162
+ root = repo_config.root_source_path || '/'
163
+
164
+ # If we have a root defined, jump right into it
165
+ if root != '/'
166
+ overlay_directory(root, repo_config)
167
+ else
168
+ root_entries = repo_config.github_api.contents.get(repo_config.org, repo_config.repo, root, ref: repo_config.branch).response.body
169
+
170
+ # We aren't pulling anything out of root. Cycle through directories and overlay
171
+ root_entries.each do |entry|
172
+ if entry.type == 'dir'
173
+ overlay_directory(entry.path, repo_config)
75
174
  end
76
175
  end
77
- Rails.logger.info "Finished processing repo with config #{repo_config.inspect}"
78
176
  end
177
+ Rails.logger.info "Overlay finished processing repo with config #{repo_config.inspect}"
79
178
  end
80
179
 
81
- def self.overlay_directory path, repo_config
82
- root_path = repo_config[:root_dest_path].empty? ? "#{Rails.application.root}" : "#{Rails.application.root}/#{repo_config[:root_dest_path]}"
83
- dynamic_path = path.partition(repo_config[:root_source_path]).last
84
-
85
- FileUtils.mkdir_p "#{root_path}/#{dynamic_path}"
86
- directory_entries = github_repo.contents.get(repo_config[:user], repo_config[:repo], path, ref: repo_config[:branch]).response.body
180
+ def overlay_directory path, repo_config
181
+ FileUtils.mkdir_p destination_path(path, repo_config)
182
+ directory_entries = repo_config.github_api.contents.get(repo_config.org, repo_config.repo, path, ref: repo_config.branch).response.body
87
183
 
88
184
  directory_entries.each do |entry|
89
185
  if entry.type == 'dir'
@@ -94,36 +190,60 @@ module Overlay
94
190
  end
95
191
  end
96
192
 
97
- def self.clone_file path, repo_config
98
- root_path = repo_config[:root_dest_path].empty? ? "#{Rails.application.root}" : "#{Rails.application.root}/#{repo_config[:root_dest_path]}"
99
- dynamic_path = path.partition(repo_config[:root_source_path]).last
193
+ def clone_file path, repo_config
194
+ file = repo_config.github_api.contents.get(repo_config.org, repo_config.repo, path, ref: repo_config.branch).response.body.content
195
+ File.open(destination_path(path, repo_config), "wb") { |f| f.write(Base64.decode64(file)) }
196
+ Rails.logger.info "Overlay cloned file: #{path}"
197
+ end
198
+
199
+ # Fork a new process and subscribe to a redis channel
200
+ def subscribe_to_channel key, repo_config
201
+ redis = Redis.new(:timeout => 30, :host => repo_config.redis_server, :port => repo_config.redis_port)
202
+
203
+ # This key is going to receive a publish event
204
+ # for any changes to the target repo. We need to verify
205
+ # that the payload references our branch and our watch direstory
206
+ redis.subscribe(key) do |on|
207
+ on.message do |channel, msg|
208
+ Rails.logger.info "Overlay received publish event for channel #{publish_key} with payload: #{msg}"
209
+ hook = JSON.parse(msg)
210
+
211
+ # Make sure this is the branch we are watching
212
+ if (hook['ref'] == "refs/heads/#{repo_config.branch}")
213
+ Rails.logger.info "Overlay enqueueing Github hook processing job for repo: #{repo_config.repo} and branch: #{repo_config.branch}"
214
+ process_hook(hook, repo_config)
215
+ Rails.logger.info "Overlay done processing job for repo: #{repo_config.repo} and branch: #{repo_config.branch}"
216
+ end
217
+ end
218
+ end
219
+ end
100
220
 
101
- file = github_repo.contents.get(repo_config[:user], repo_config[:repo], path, ref: repo_config[:branch]).response.body.content
102
- File.open("#{root_path}/#{dynamic_path}", "wb") { |f| f.write(Base64.decode64(file)) }
221
+ def my_file? file_path, repo_config
222
+ return true if repo_config.root_dest_path.empty?
223
+ file_path.starts_with?(repo_config.root_source_path)
103
224
  end
104
225
 
105
- private
226
+ def root_path repo_config
227
+ repo_config.root_dest_path.empty? ? "#{Rails.application.root}" : "#{Rails.application.root}/#{repo_config.root_dest_path}"
228
+ end
106
229
 
107
- def self.github_repo
108
- @@github ||= ::Github::Repos.new
230
+ def destination_path file_path, repo_config
231
+ raise "The file #{file_path} isn't handled by this repo with root path: #{repo_config.root_source_path}" unless my_file?(file_path, repo_config)
232
+ dynamic_path = file_path.partition(repo_config.root_source_path).last
233
+ return "#{root_path(repo_config)}#{dynamic_path}"
109
234
  end
110
235
 
111
- def self.config
112
- @@overlay_config ||= Overlay.configuration
236
+ def config
237
+ @overlay_config ||= Overlay.configuration
113
238
  end
114
239
 
115
- # Configure the github api
116
- def self.configure
117
- # Validate required config
118
- raise 'Configuration github_overlays.basic_auth not set' if (!config.auth || config.auth.nil?)
119
-
120
- ::Github.configure do |github_config|
121
- github_config.endpoint = config.endpoint if config.endpoint
122
- github_config.site = config.site if config.site
123
- github_config.basic_auth = config.auth
124
- github_config.adapter = :net_http
125
- github_config.ssl = {:verify => false}
240
+ # Process the passed in function symbol and args in a fork
241
+ def fork_it method, *args
242
+ pid = Process.fork do
243
+ send(method, *args)
244
+ Process.exit
126
245
  end
246
+ Process.detach(pid)
127
247
  end
128
248
  end
129
249
  end