dokuen 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -44,6 +44,8 @@ Create a `git` user and install gitolite according to [package directions](http:
44
44
  Run this:
45
45
 
46
46
  ```
47
+ $ sudo mkdir -p /usr/local/var/dokuen
48
+ $ cd /usr/local/var/dokuen
47
49
  $ sudo dokuen setup
48
50
  ```
49
51
 
@@ -108,3 +110,4 @@ $ ssh git@<your_host> dokuen restart_app <name>
108
110
 
109
111
  Unfortunately the stock Heroku buildpacks install a vendored node.js compiled for the Heroku platform, which happens to be linux. This doesn't work for Mac, which means you have to use a slightly patched version. This one works with a homebrew-installed node.js: https://github.com/peterkeen/heroku-buildpack-ruby
110
112
 
113
+
data/bin/dokuen CHANGED
@@ -12,4 +12,4 @@ rescue LoadError => e
12
12
  require 'dokuen'
13
13
  end
14
14
 
15
- Dokuen::Cli.start()
15
+ Dokuen::CLI.start()
@@ -1,6 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'rubygems'
4
- require 'dokuen'
5
-
6
- Dokuen.sys("/usr/local/sbin/nginx -s reload")
3
+ system("/usr/local/sbin/nginx -s reload") or raise "Error restarting nginx!"
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ <%= @current_script %> $@ --config=<%= File.expand_path("dokuen.conf") %>
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ sudo -u <%= options[:appuser] %> <%= File.expand_path("./bin/dokuen") %> deploy --application=$APP --rev=$REV
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+
3
+ sudo -u <%= options[:appuser] %> <%= File.expand_path("./bin/dokuen") %> $@
4
+
@@ -0,0 +1,24 @@
1
+ ==== IMPORTANT INSTRUCTIONS ====
2
+
3
+ In your .gitolite.rc file, in the COMMANDS section, add the following:
4
+
5
+ 'dokuen' => 1
6
+
7
+ In your gitolite.conf file, add the following:
8
+
9
+ repo apps/[a-zA-Z0-9].*
10
+ C = @all
11
+ RW+ = CREATOR
12
+ config hooks.pre = "<%= File.expand_path('./bin/dokuen-deploy') %>"
13
+
14
+ In your nginx.conf, add the following to your http section:
15
+
16
+ include "<%= File.expand_path('nginx') %>/*.conf";
17
+
18
+ Run "sudo visudo" and add the following lines:
19
+
20
+ Runas_Alias APPUSERS = <%= options[:appuser] %>
21
+
22
+ git ALL=(APPUSERS) NOPASSWD: <%= File.expand_path('./bin/dokuen') %>
23
+ <%= options[:appuser] %> ALL=NOPASSWD: <%= @current_bin_path %>/dokuen_restart_nginx
24
+
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>info.bugpslat.dokuen</string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string><%= File.expand_path("./bin/dokuen") %></string>
10
+ <string>boot</string>
11
+ </array>
12
+ <key>RunAtLoad</key>
13
+ <true/>
14
+ <key>WorkingDirectory</key>
15
+ <string><%= File.expand_path(".") %></string>
16
+ <key>UserName</key>
17
+ <string<%= options[:appuser] %></string>
18
+ </dict>
19
+ </plist>
@@ -0,0 +1,22 @@
1
+ <% if ports.length > 0 %>
2
+ upstream <%= name %> {
3
+ <% ports.each do |port| %>
4
+ server localhost:<%= port %>;
5
+ <% end %>
6
+ }
7
+
8
+ server {
9
+ listen 80;
10
+ listen 443 default ssl;
11
+
12
+ server_name <%= name %>.<%= config.base_domain_name %>;
13
+
14
+ if ($ssl_protocol = "") {
15
+ rewrite ^ https://$server_name$request_uri? permanent;
16
+ }
17
+
18
+ location / {
19
+ proxy_pass http://<%= name %>;
20
+ }
21
+ }
22
+ <% end %>
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ hook = `git config hooks.pre`.chomp
3
+
4
+ rev = nil
5
+
6
+ STDIN.each do |line|
7
+ parts = line.split(/\s/)
8
+ next if parts[2] != "refs/heads/master"
9
+ rev = parts[1]
10
+ end
11
+
12
+ if hook != ""
13
+ name = File.basename(ENV['GL_REPO']).gsub(".git", '')
14
+ cmd = "env REV=#{rev} APP=#{name} #{hook}"
15
+ system(cmd) or raise "Error running pre-hook: #{cmd} returned #{$?}"
16
+ end
@@ -0,0 +1,5 @@
1
+ start at startup
2
+
3
+ task
4
+
5
+ exec sudo -u <%= options[:appuser] %> <%= File.expand_path("./bin/dokuen") %> boot
@@ -0,0 +1,279 @@
1
+ require 'tmpdir'
2
+ require 'fileutils'
3
+ require 'time'
4
+
5
+ class Dokuen::Application
6
+
7
+ attr_reader :name, :config
8
+
9
+ def initialize(name, config)
10
+ @name = name
11
+ @config = config
12
+ end
13
+
14
+ def get_env(var)
15
+ with_app_dir do
16
+ env_path = File.join("env", var)
17
+ if File.exists?(env_path)
18
+ return File.read(env_path)
19
+ else
20
+ return nil
21
+ end
22
+ end
23
+ end
24
+
25
+ def set_env(var, val)
26
+ with_app_dir do
27
+ env_path = File.join("env", var)
28
+ File.open(env_path, "w+") do |f|
29
+ f.write(val)
30
+ end
31
+ end
32
+ end
33
+
34
+ def delete_env(var)
35
+ with_app_dir do
36
+ File.delete(File.join("env", var)) rescue nil
37
+ end
38
+ end
39
+
40
+ def env
41
+ vars = read_env_dir("#{config.dokuen_dir}/env")
42
+ with_app_dir do
43
+ vars.merge!(read_env_dir('env'))
44
+ with_current_release do
45
+ File.open(".env") do |f|
46
+ f.lines.each do |line|
47
+ key, val = line.split('=', 2)
48
+ vars[key] = val.chomp
49
+ end
50
+ end
51
+ end
52
+ end
53
+ vars
54
+ end
55
+
56
+ def create
57
+ Dir.chdir(File.join(config.dokuen_dir, 'apps')) do
58
+ if File.exists?(name)
59
+ raise "Application #{name} exists!"
60
+ end
61
+
62
+ FileUtils.mkdir_p(name)
63
+ with_app_dir do
64
+ dirs = [
65
+ 'releases',
66
+ 'env',
67
+ 'logs',
68
+ 'build'
69
+ ]
70
+ FileUtils.mkdir_p(dirs)
71
+ end
72
+ end
73
+ end
74
+
75
+ def deploy(revision)
76
+ git_dir = Dir.getwd
77
+
78
+ with_app_dir do
79
+ now = Time.now().utc().strftime("%Y%m%dT%H%M%S")
80
+ release_dir = "releases/#{now}"
81
+ clone_dir = clone(git_dir, revision)
82
+
83
+ buildpack = get_env('BUILDPACK_URL')
84
+ if buildpack
85
+ buildpack = "-b #{buildpack}"
86
+ else
87
+ buildpack = ''
88
+ end
89
+
90
+ sys("mason build #{clone_dir} #{buildpack} -o #{release_dir} -c build")
91
+ Dir.mkdir("#{release_dir}/.dokuen")
92
+
93
+ hook = get_env('DOKUEN_AFTER_BUILD')
94
+ if hook
95
+ sys("foreman run #{hook}")
96
+ end
97
+
98
+ if File.symlink?("previous")
99
+ File.unlink("previous")
100
+ File.symlink(File.readlink("current"), "previous")
101
+ end
102
+
103
+ if File.symlink?("current")
104
+ File.unlink("current")
105
+ end
106
+
107
+ File.symlink(File.expand_path(release_dir), "current")
108
+ end
109
+
110
+ scale
111
+ if File.symlink?("previous")
112
+ shutdown(File.readlink("previous"))
113
+ end
114
+ end
115
+
116
+ def scale
117
+ puts "Scaling..."
118
+ with_current_release do
119
+ processes = running_processes
120
+ running_count_by_name = {}
121
+
122
+ processes.each do |proc, pidfile|
123
+ proc_name = proc.split('.')[0]
124
+ running_count_by_name[proc_name] ||= 0
125
+ running_count_by_name[proc_name] += 1
126
+ end
127
+
128
+ desired_count_by_name = {}
129
+ scale_spec = get_env('DOKUEN_SCALE')
130
+ if scale_spec
131
+ scale_spec.split(',').each do |spec|
132
+ proc_name, count = spec.split('=')
133
+ desired_count_by_name[proc_name] = count.to_i
134
+ end
135
+ end
136
+
137
+ to_start = []
138
+ to_stop = []
139
+
140
+ desired_count_by_name.each do |proc_name, count|
141
+ running = running_count_by_name[proc_name] || 0
142
+ if running < count
143
+ (count - running).times do |i|
144
+ index = running + i + 1
145
+ to_start << [proc_name, index]
146
+ end
147
+ elsif running > count
148
+ (running - count).times do |i|
149
+ index = count + i + 1
150
+ to_stop << [proc_name, index]
151
+ end
152
+ end
153
+ end
154
+
155
+ running_count_by_name.each do |proc_name, count|
156
+ if not desired_count_by_name.has_key?(proc_name)
157
+ count.times do |i|
158
+ to_stop << [proc_name, i]
159
+ end
160
+ end
161
+ end
162
+
163
+ to_start.each do |proc_name, index|
164
+ port = reserve_port
165
+ fork do
166
+ Dokuen::Wrapper.new(self, proc_name, index, File.join(config.dokuen_dir, 'ports', port.to_s)).run!
167
+ end
168
+ end
169
+
170
+ to_stop.each do |proc_name, index|
171
+ pid_file = processes["#{proc_name}.#{index}"]
172
+ pid = YAML.load(File.read(pid_file))['pid']
173
+ Process.kill("TERM", pid)
174
+ end
175
+ install_nginx_config
176
+ end
177
+ end
178
+
179
+ def shutdown(release)
180
+ with_release_dir(release) do
181
+ running_processes.each do |proc, pidfile|
182
+ pid = File.read(pidfile).to_i rescue nil
183
+ if pid
184
+ Process.kill("TERM", pid)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ def install_nginx_config
191
+ puts "Installing nginx config"
192
+ sleep 2
193
+ @ssl_on = get_env('USE_SSL') ? 'on' : 'off'
194
+ @listen_port = get_env('USE_SSL') ? 443 : 80
195
+ conf = Dokuen.template('nginx', binding)
196
+ File.open(File.join(config.dokuen_dir, "nginx", "#{name}.#{config.base_domain_name}.conf"), "w+") do |f|
197
+ f.write(conf)
198
+ end
199
+
200
+ sys("sudo #{config.bin_path}/dokuen_restart_nginx")
201
+ end
202
+
203
+ private
204
+
205
+ def clone(git_dir, revision)
206
+ dir = Dir.mktmpdir
207
+ Dir.chdir(dir) do
208
+ sys("git clone #{git_dir} .")
209
+ sys("git checkout -q #{revision}")
210
+ end
211
+ dir
212
+ end
213
+
214
+ def sys(command)
215
+ system(command) or raise "Error running command: #{command}"
216
+ end
217
+
218
+ def with_app_dir
219
+ Dir.chdir(File.join(config.dokuen_dir, 'apps', name)) do
220
+ yield
221
+ end
222
+ end
223
+
224
+ def with_release_dir(release)
225
+ Dir.chdir(release) do
226
+ yield
227
+ end
228
+ end
229
+
230
+ def with_current_release
231
+ with_app_dir do
232
+ if File.symlink?("current")
233
+ with_release_dir(File.readlink("current")) do
234
+ yield
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ def running_processes
241
+ procs = {}
242
+ Dir.glob(".dokuen/*.pid").map do |pidfile|
243
+ proc_name = File.basename(pidfile).gsub('.pid', '')
244
+ proc_name = proc_name.gsub("dokuen.#{name}.", '')
245
+ procs[proc_name] = pidfile
246
+ end
247
+ procs
248
+ end
249
+
250
+ def read_env_dir(dir)
251
+ vars = {}
252
+ Dir.glob("#{dir}/*").each do |key|
253
+ vars[File.basename(key)] = File.read(key).chomp
254
+ end
255
+ vars
256
+ end
257
+
258
+ def reserve_port
259
+ ports_dir = config.dokuen_dir + "/ports"
260
+ port_range = config.max_port - config.min_port
261
+ 1000.times do
262
+ port = rand(port_range) + config.min_port
263
+ path = File.join(ports_dir, port.to_s)
264
+ if not File.exists?(path)
265
+ FileUtils.touch(path)
266
+ return port
267
+ end
268
+ end
269
+ raise "Could not find free port!"
270
+ end
271
+
272
+ def ports
273
+ _ports = []
274
+ running_processes.each do |proc_name, pidfile|
275
+ _ports << YAML.load(File.read(pidfile))['port']
276
+ end
277
+ _ports
278
+ end
279
+ end
data/lib/dokuen/cli.rb CHANGED
@@ -1,226 +1,163 @@
1
- require 'rubygems'
2
- require 'thor'
3
- require 'fileutils'
1
+ require "thor"
2
+ require "thor/shell/basic"
3
+ require "yaml"
4
4
 
5
- module Dokuen
6
- class Cli < Thor
7
- include Thor::Actions
8
-
9
- desc "setup", "Set up relevant things. Needs to be run as sudo."
10
- def setup
11
- raise "Must be run as root" unless Process.uid == 0
12
- git_username = ask("What is your git username (usually git)?")
13
- gitolite_src = ask("What is the path to your gitolite clone?")
14
- say("Installing gitolite command")
15
- create_file "#{gitolite_src}/src/commands/dokuen", <<HERE
16
- #!/bin/sh
17
- /usr/local/bin/dokuen $@
18
- HERE
19
- File.chmod(0755, "#{gitolite_src}/src/commands/dokuen")
20
- say("Installing gitolite hook")
21
- hook_path = "/Users/#{git_username}/.gitolite/hooks/common/pre-receive"
22
- create_file(hook_path, <<'HERE'
23
- #!/usr/bin/env ruby
24
- hook = `git config hooks.pre`.chomp
25
-
26
- rev = nil
27
-
28
- STDIN.each do |line|
29
- parts = line.split(/\s/)
30
- next if parts[2] != "refs/heads/master"
31
- rev = parts[1]
32
- end
33
-
34
- if hook != ""
35
- name = File.basename(ENV['GL_REPO'])
36
- cmd = "#{hook} #{name} #{rev}"
37
- system(cmd) or raise "Error running pre-hook: #{cmd} returned #{$?}"
38
- end
39
- HERE
40
- )
41
- File.chmod(0755, hook_path)
42
- say("Creating directories")
43
- dirs = [
44
- '/usr/local/var/dokuen/env',
45
- '/usr/local/var/dokuen/env/_common',
46
- '/usr/local/var/dokuen/log',
47
- '/usr/local/var/dokuen/nginx',
48
- '/usr/local/var/dokuen/build',
49
- '/usr/local/var/dokuen/release',
50
- ]
51
- FileUtils.mkdir_p(dirs, :mode => 0775)
52
- FileUtils.chown(git_username, 'staff', dirs)
53
-
54
- if yes?("Do you want to set up DNS?")
55
- basename = ask("What is your DNS base domain? For example, if have a wildcard CNAME for *.example.com, this would be example.com.")
56
- create_file("/usr/local/var/dokuen/env/_common/BASE_DOMAIN", basename)
57
- end
58
- git_hostname = `hostname`.chomp
59
- if no?("Is #{git_hostname} the correct hostname for git remotes?")
60
- git_hostname = ask("What hostname should we use instead?")
61
- end
62
- create_file("/usr/local/var/dokuen/env/_common/DOKUEN_GIT_SERVER", git_hostname)
63
- create_file("/usr/local/var/dokuen/env/_common/DOKUEN_GIT_USER", git_username)
64
-
65
- say(<<HERE
66
-
67
- ==== IMPORTANT INSTRUCTIONS ====
68
-
69
- In your .gitolite.rc file, in the COMMANDS section, add the following:
70
-
71
- 'dokuen' => 1
5
+ class Dokuen::CLI < Thor
72
6
 
73
- In your gitolite.conf file, add the following:
7
+ include Thor::Actions
74
8
 
75
- repo apps/[a-zA-Z0-9].*
76
- C = @all
77
- RW+ = CREATOR
78
- config hooks.pre = "/usr/local/bin/dokuen deploy"
9
+ def initialize(*args)
10
+ super(*args)
79
11
 
80
- In your nginx.conf, add the following to your http section:
12
+ if not options[:config].nil?
13
+ @config = Dokuen::Config.new(options[:config])
14
+ end
15
+ end
81
16
 
82
- include "/usr/local/var/dokuen/nginx/*.conf";
17
+ class_option :application, :type => :string, :desc => "Application name"
18
+ class_option :config, :type => :string, :desc => "Configuration file"
19
+ class_option :debug, :type => :boolean, :desc => "Show backtraces"
20
+
21
+ desc "setup DIR", "set up dokuen in the given directory"
22
+ method_option :gituser, :desc => "Username of git user", :default => 'git'
23
+ method_option :gitgroup, :desc => "Group of git user", :default => 'staff'
24
+ method_option :appuser, :desc => "Username of app user", :default => 'dokuen'
25
+ method_option :gitolite, :desc => "Path to gitolite directory", :default => 'GITUSER_HOME/gitolite'
26
+ method_option :platform, :desc => "Which platform to install. Can be 'mac', 'ubuntu', or 'centos'", :default => 'mac'
27
+ def setup(dir)
28
+
29
+ @current_script = File.expand_path($0)
30
+ @current_bin_path = File.dirname(@current_script)
31
+
32
+ Dir.chdir(dir) do
33
+ setup_dirs
34
+ setup_bin
35
+ setup_gitolite
36
+ write_config
37
+ install_boot_script
38
+ puts Dokuen.template('instructions', binding)
39
+ end
40
+ end
83
41
 
84
- Run "sudo visudo" and add the following line:
42
+ desc "create", "create application"
43
+ def create
44
+ Dokuen::Application.new(options[:application], @config).create
45
+ puts "git remote add dokuen #{@config.git_user}@#{@config.git_server}:apps/#{options[:application]}.git"
46
+ end
85
47
 
86
- git ALL=NOPASSWD: /usr/local/bin/dokuen_install_launchdaemon, /usr/local/bin/dokuen_restart_nginx
48
+ desc "deploy", "deploy application", :hide => true
49
+ method_option :rev, :desc => "Revision to deploy"
50
+ def deploy
51
+ ENV['GIT_DIR'] = nil
52
+ ENV['PATH'] = "#{@config.bin_path}:#{ENV['PATH']}"
53
+ ENV.each do |k,v|
54
+ puts "#{k}=#{v}"
55
+ end
56
+ Dokuen::Application.new(options[:application], @config).deploy(options[:rev])
57
+ end
87
58
 
88
- HERE
89
- )
59
+ desc "scale SCALE_SPEC", "scale to the given spec"
60
+ def scale(spec)
61
+ app = Dokuen::Application.new(options[:application], @config)
62
+ app.set_env('DOKUEN_SCALE', spec)
63
+ app.scale
64
+ end
90
65
 
66
+ desc "config", "show the config for the given app"
67
+ def config
68
+ app = Dokuen::Application.new(options[:application], @config)
69
+ app.env.each do |key, val|
70
+ puts "#{key}=#{val}"
91
71
  end
72
+ end
92
73
 
93
- desc "create [APP]", "Create a new application."
94
- def create(app="")
95
- raise "app name required" if app.nil? || app == ""
96
- read_env(app)
97
- FileUtils.mkdir_p(Dokuen.dir("env", app))
98
- FileUtils.mkdir_p(Dokuen.dir("release", app))
99
- FileUtils.mkdir_p(Dokuen.dir("build", app))
100
- puts "Created new application named #{app}"
101
- puts "Git remote: #{Dokuen.base_clone_url}:apps/#{app}.git"
74
+ desc "config_set VARS", "set some config variables"
75
+ def config_set(*vars)
76
+ app = Dokuen::Application.new(options[:application], @config)
77
+ vars.each do |var|
78
+ key, val = var.chomp.split('=', 2)
79
+ app.set_env(key, val)
102
80
  end
81
+ end
103
82
 
104
- desc "restart_app [APP]", "Restart an existing app"
105
- def restart_app(app="")
106
- check_app(app)
107
- read_env(app)
108
- deploy = Dokuen::Deploy.new(app, '', ENV['DOKUEN_RELEASE_DIR'])
109
- deploy.install_launch_daemon
110
- deploy.install_nginx_conf
111
- puts "App restarted"
83
+ desc "config_delete VARS", "delete some config variables"
84
+ def config_delete(*vars)
85
+ app = Dokuen::Application.new(options[:application], @config)
86
+ vars.each do |var|
87
+ app.delete_env(var)
112
88
  end
89
+ end
113
90
 
114
- desc "start_app [APP]", "Start an app", :hide => true
115
- def start_app(app="")
116
- check_app(app)
117
- read_env(app)
118
- ENV['PATH'] = "/usr/local/bin:#{ENV['PATH']}"
119
- Dir.chdir(ENV['DOKUEN_RELEASE_DIR']) do
120
- base_port = ENV['PORT'].to_i - 200
121
- scale = ENV['DOKUEN_SCALE'].nil? ? "" : "-c #{ENV['DOKUEN_SCALE']}"
122
- Dokuen.sys("foreman start #{scale} -p #{base_port}")
123
- end
91
+ desc "boot", "Scale all of the current applications"
92
+ def boot
93
+ Dir.glob("#{@config.dokuen_dir}/apps/*") do |appdir|
94
+ next if File.basename(appdir)[0] == '.'
95
+ app = Dokuen::Application.new(File.basename(appdir), @config)
96
+ app.scale
124
97
  end
98
+ end
125
99
 
126
- desc "scale [APP] [SCALE_SPEC]", "Scale an app to the given spec"
127
- def scale(app="", scale_spec="")
128
- check_app(app)
129
- raise "scale spec required" if scale_spec == ""
130
- read_env(app)
131
- Dokuen.set_env(app, 'DOKUEN_SCALE', scale_spec)
132
- restart_app(app)
133
- puts "Scaled to #{scale_spec}"
134
- end
100
+ private
101
+
102
+ def setup_dirs
103
+ dirs = [
104
+ 'apps',
105
+ 'env',
106
+ 'ports',
107
+ 'nginx',
108
+ 'bin'
109
+ ]
135
110
 
136
- desc "deploy [APP] [REV]", "Deploy an app for a given revision. Run within git pre-receive.", :hide => true
137
- def deploy(app="", rev="")
138
- check_app(app)
139
- read_env(app)
140
- Dokuen::Deploy.new(app, rev).run
141
- puts "App #{app} deployed"
111
+ dirs.each do |dir|
112
+ empty_directory(File.join(Dir.getwd, dir))
142
113
  end
143
114
 
144
- desc "run_command [APP]", "Run a command in the given app's environment"
145
- method_option :command, :aliases => '-C', :desc => "Command to run"
146
- def run_command(app="")
147
- check_app(app)
148
- read_env(app)
115
+ FileUtils.chown(options[:gituser], options[:gitgroup], dirs)
116
+ FileUtils.chmod(0777, ['apps', 'ports', 'nginx'])
117
+ end
149
118
 
150
- ENV['PATH'] = "/usr/local/bin:#{ENV['PATH']}"
151
- Dir.chdir(ENV['DOKUEN_RELEASE_DIR']) do
152
- Dokuen.sys("foreman run #{options[:command]}")
153
- end
154
- end
119
+ def setup_bin
120
+ @script_path = File.expand_path("bin/dokuen")
121
+ @deploy_script_path = File.expand_path("bin/dokuen-deploy")
122
+ write_template(@script_path, "bin_command", 0755)
123
+ write_template(@deploy_script_path, "deploy_command", 0755)
124
+ end
155
125
 
156
- desc "config [APP] [set/delete]", "Add or remove config variables"
157
- method_option :vars, :aliases => '-V', :desc => "Variables to set or remove", :type => :array
158
- def config(app="", subcommand="")
159
- check_app(app)
160
- case subcommand
161
- when "set"
162
- set_vars(app)
163
- restart_app(app)
164
- when "delete"
165
- delete_vars(app)
166
- restart_app(app)
167
- else
168
- show_vars(app)
169
- end
170
- end
126
+ def setup_gitolite
127
+ githome = File.expand_path("~#{options[:gituser]}")
128
+ gitolite = options[:gitolite].gsub('GITUSER_HOME', githome)
171
129
 
172
- desc "install_buildpack [URL]", "Add a buildpack to the mason config"
173
- def install_buildpack(url="")
174
- raise "URL required" unless url != ""
175
- Dokuen.sys("/usr/local/bin/mason buildpacks:install #{url}")
176
- end
130
+ write_template("#{gitolite}/src/commands/dokuen", 'gitolite_command', 0755)
131
+ write_template("#{githome}/.gitolite/hooks/common/pre-receive", 'pre_receive_hook', 0755)
132
+ end
177
133
 
178
- desc "remove_buildpack [NAME]", "Remove a buildpack from the mason config"
179
- def remove_buildpack(name)
180
- raise "Name required" unless name != ""
181
- Dokuen.sys("/usr/local/bin/mason buildpacks:uninstall #{name}")
134
+ def write_config
135
+ config = {
136
+ 'base_domain_name' => 'dokuen',
137
+ 'git_server' => `hostname`.chomp,
138
+ 'git_user' => options[:gituser],
139
+ 'app_user' => options[:appuser],
140
+ 'min_port' => 5000,
141
+ 'max_port' => 6000,
142
+ 'bin_path' => @current_bin_path,
143
+ 'dokuen_dir' => File.expand_path('.')
144
+ }
145
+
146
+ File.open("./dokuen.conf", 'w+') do |f|
147
+ YAML.dump(config, f)
182
148
  end
149
+
150
+ end
183
151
 
184
- desc "buildpacks", "List the available buildpacks"
185
- def buildpacks
186
- Dokuen.sys("/usr/local/bin/mason buildpacks")
187
- end
152
+ def write_template(filename, template, mode=0644)
153
+ t = Dokuen.template(template, binding)
154
+ create_file(filename, t)
155
+ File.chmod(mode, filename)
156
+ end
188
157
 
189
- no_tasks do
190
-
191
- def set_vars(app)
192
- vars = options[:vars]
193
- vars.each do |var|
194
- key, val = var.split(/\=/)
195
- Dokuen.set_env(app, key, val)
196
- end
197
- puts "Vars set"
198
- end
199
-
200
- def delete_vars(app)
201
- vars = options[:vars]
202
-
203
- vars.each do |var|
204
- Dokuen.rm_env(app, var)
205
- end
206
- puts "Vars removed"
207
- end
208
-
209
- def show_vars(app)
210
- read_env(app)
211
- ENV.each do |key, val|
212
- puts "#{key}=#{val}"
213
- end
214
- end
215
-
216
- def read_env(app)
217
- Dokuen.read_env("_common")
218
- Dokuen.read_env(app)
219
- end
220
-
221
- def check_app(app)
222
- Dokuen.app_exists?(app) or raise "App '#{app}' does not exist!"
223
- end
224
- end
158
+ def install_boot_script
159
+ filename, template_name = Dokuen::Platform.boot_script(options[:platform])
160
+ write_template(filename, template_name)
225
161
  end
162
+
226
163
  end
@@ -0,0 +1,23 @@
1
+ require "yaml"
2
+
3
+ class Dokuen::Config
4
+ def initialize(path)
5
+ @path = path
6
+ @config = {}
7
+ read_config
8
+ end
9
+
10
+ def read_config
11
+ @config = YAML.load(File.read(@path))
12
+ end
13
+
14
+ def method_missing(m, *args, &block)
15
+ str_meth = m.to_s
16
+ if @config.has_key? str_meth
17
+ @config[str_meth]
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,16 @@
1
+ module Dokuen
2
+ module Platform
3
+
4
+ def self.boot_script(platform)
5
+ case platform
6
+ when 'mac'
7
+ return "/Library/LaunchDaemons/dokuen.plist", "mac_launchdaemon"
8
+ when 'ubuntu'
9
+ return "/etc/init/dokuen", "ubuntu_upstart"
10
+ else
11
+ raise "Unknow platform: #{platform}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -1,3 +1,3 @@
1
1
  module Dokuen
2
- VERSION = '0.0.4'
2
+ VERSION = '0.0.5'
3
3
  end
@@ -0,0 +1,127 @@
1
+ require 'foreman'
2
+ require 'foreman/procfile'
3
+ require 'foreman/process'
4
+
5
+ class Dokuen::Wrapper
6
+
7
+ attr_reader :release_dir, :app, :proc, :index, :portfile
8
+
9
+ def initialize(app, proc, index, portfile)
10
+ @release_dir = Dir.getwd
11
+ @app = app
12
+ @proc = proc
13
+ @index = index
14
+ @portfile = portfile
15
+ end
16
+
17
+ def run!
18
+ return if daemonize! == nil
19
+ load_env
20
+ loop do
21
+ run_loop
22
+ end
23
+ end
24
+
25
+ def procname
26
+ "dokuen.#{app.name}.#{proc}.#{index}"
27
+ end
28
+
29
+ def procdir
30
+ File.join(release_dir, '.dokuen')
31
+ end
32
+
33
+ def pidfile
34
+ File.join(procdir, "#{procname}.pid")
35
+ end
36
+
37
+ def outfile
38
+ File.join(procdir, "#{procname}.out")
39
+ end
40
+
41
+ def errfile
42
+ File.join(procdir, "#{procname}.err")
43
+ end
44
+
45
+ def port
46
+ File.basename(portfile).to_i
47
+ end
48
+
49
+ def daemonize!
50
+ pf = File.join(pidfile)
51
+ raise "Process already running!" if File.exists?(pf)
52
+
53
+ return unless do_fork.nil?
54
+ write_pidfile
55
+ set_procname
56
+ redirect
57
+ end
58
+
59
+ def redirect
60
+ $stdin.reopen("/dev/null")
61
+ $stdout.sync = $stderr.sync = true
62
+ $stdout.reopen(File.new(outfile, "a"))
63
+ $stderr.reopen(File.new(errfile, "a"))
64
+ end
65
+
66
+ def write_pidfile
67
+ File.open(pidfile, "w+") do |f|
68
+ f.write(YAML.dump({'pid' => Process.pid, 'port' => port}))
69
+ end
70
+ end
71
+
72
+ def set_procname
73
+ $0 = procname
74
+ end
75
+
76
+ def do_fork
77
+ raise 'first fork failed' if (pid = fork) == -1
78
+ exit unless pid.nil?
79
+ Process.setsid
80
+ raise 'second fork failed' if (pid = fork) == -1
81
+ exit unless pid.nil?
82
+ end
83
+
84
+ def load_env
85
+ app.env.each do |key, val|
86
+ ENV[key] = val
87
+ end
88
+ end
89
+
90
+ def run_loop
91
+ reader, writer = (IO.method(:pipe).arity == 0 ? IO.pipe : IO.pipe("BINARY"))
92
+ procfile = Foreman::Procfile.new("Procfile")
93
+ entry = procfile[proc]
94
+ process = Foreman::Process.new(entry, index.to_i, port.to_i)
95
+ log_path = "../../logs/#{procname}"
96
+ log_file = File.open(log_path, 'a')
97
+
98
+ Signal.trap("USR2") do
99
+ process.kill("TERM")
100
+ end
101
+
102
+ Signal.trap("TERM") do
103
+ if not process.kill(9)
104
+ raise "Failed to kill process #{process.pid}"
105
+ end
106
+ File.delete(pidfile)
107
+ File.delete("../../../../ports/#{port}")
108
+ exit! 0
109
+ end
110
+
111
+ process.run(writer, release_dir, {})
112
+ thread = Thread.new do
113
+ loop do
114
+ data = reader.gets
115
+ next unless data
116
+ ps, message = data.split(",", 2)
117
+ log_file.puts("[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}][#{ps}] #{message}")
118
+ log_file.flush
119
+ end
120
+ end
121
+ Process.wait(process.pid)
122
+ thread.exit
123
+ reader.close
124
+ writer.close
125
+ end
126
+
127
+ end
data/lib/dokuen.rb CHANGED
@@ -1,46 +1,19 @@
1
- require 'dokuen/cli'
2
- require 'dokuen/deploy'
1
+ require "erb"
3
2
 
4
3
  module Dokuen
5
- def self.dir(name, app=nil)
6
- parts = ["/usr/local/var/dokuen", name]
7
-
8
- if not app.nil?
9
- parts << app
10
- end
11
-
12
- File.join(parts)
13
- end
14
-
15
- def self.sys(command)
16
- system(command) or raise "Error running #{command}"
17
- end
18
-
19
- def self.read_env(name)
20
- env_dir = Dokuen.dir('env', name)
21
- Dir.glob("#{env_dir}/*") do |var|
22
- var_name = File.basename(var)
23
- ENV[var_name] = File.open(var).read().chomp()
24
- end
25
- end
26
-
27
- def self.set_env(name, key, value)
28
- env_dir = Dokuen.dir('env', name)
29
- File.open(File.join(env_dir, key), "w+") do |f|
30
- f.write value
4
+ def self.template(name, bind)
5
+ path = File.expand_path("../../data/templates/#{name}.erb", __FILE__)
6
+ if File.exists?(path)
7
+ t = ERB.new(File.read(path))
8
+ t.result(bind)
9
+ else
10
+ raise "Unknown template: #{name}"
31
11
  end
32
12
  end
33
-
34
- def self.rm_env(name, key)
35
- env_dir = Dokuen.dir('env', name)
36
- File.delete(File.join(env_dir, key))
37
- end
38
-
39
- def self.base_clone_url
40
- "#{ENV['DOKUEN_GIT_USER']}@#{ENV['DOKUEN_GIT_SERVER']}"
41
- end
42
-
43
- def self.app_exists?(name)
44
- File.exists?(Dokuen.dir('env', name))
45
- end
46
13
  end
14
+
15
+ require "dokuen/cli"
16
+ require "dokuen/config"
17
+ require "dokuen/application"
18
+ require "dokuen/wrapper"
19
+ require "dokuen/platform"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dokuen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-21 00:00:00.000000000Z
12
+ date: 2012-05-29 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -91,11 +91,22 @@ files:
91
91
  - bin/dokuen
92
92
  - bin/dokuen_install_launchdaemon
93
93
  - bin/dokuen_restart_nginx
94
+ - data/templates/bin_command.erb
95
+ - data/templates/deploy_command.erb
96
+ - data/templates/gitolite_command.erb
97
+ - data/templates/instructions.erb
98
+ - data/templates/mac_launchdaemon.erb
99
+ - data/templates/nginx.erb
100
+ - data/templates/pre_receive_hook.erb
101
+ - data/templates/ubuntu_upstart.erb
94
102
  - dokuen.gemspec
95
103
  - lib/dokuen.rb
104
+ - lib/dokuen/application.rb
96
105
  - lib/dokuen/cli.rb
97
- - lib/dokuen/deploy.rb
106
+ - lib/dokuen/config.rb
107
+ - lib/dokuen/platform.rb
98
108
  - lib/dokuen/version.rb
109
+ - lib/dokuen/wrapper.rb
99
110
  homepage: https://github.com/peterkeen/dokuen
100
111
  licenses: []
101
112
  post_install_message:
data/lib/dokuen/deploy.rb DELETED
@@ -1,173 +0,0 @@
1
- require 'tmpdir'
2
- require 'fileutils'
3
- require 'time'
4
- require 'erb'
5
-
6
- module Dokuen
7
- class Deploy
8
-
9
- def initialize(app, rev, release_dir=nil)
10
- @app = app || app_name_from_env
11
- @rev = rev
12
- ENV['GIT_DIR'] = nil
13
- ENV['PATH'] = "/usr/local/bin:#{ENV['PATH']}"
14
- if release_dir
15
- @release_dir = release_dir
16
- end
17
- end
18
-
19
- def run
20
- read_app_env
21
- make_dirs
22
- clone
23
- build
24
- install_launch_daemon
25
- install_nginx_conf
26
- end
27
-
28
- def read_app_env
29
- Dokuen.read_env('_common')
30
- Dokuen.read_env(@app)
31
- end
32
-
33
- def make_dirs
34
- FileUtils.mkdir_p([
35
- env_dir,
36
- release_dir,
37
- cache_dir,
38
- nginx_dir
39
- ])
40
- end
41
-
42
- def clone
43
- Dokuen.sys("git clone #{git_dir} #{clone_dir}")
44
- Dir.chdir(clone_dir)
45
- Dokuen.sys("git checkout #{@rev}")
46
- end
47
-
48
- def build
49
- Dokuen.sys("mason build #{clone_dir} #{buildpack} -o #{release_dir} -c #{cache_dir}")
50
- Dokuen.sys("chmod -R a+r #{release_dir}")
51
- Dokuen.sys("find #{release_dir} -type d -exec chmod a+x {} \\;")
52
- Dokuen.set_env(@app, 'DOKUEN_RELEASE_DIR', release_dir)
53
-
54
- hook = ENV['DOKUEN_AFTER_BUILD']
55
- if not hook.nil?
56
- Dir.chdir release_dir do
57
- Dokuen.sys("foreman run #{hook}")
58
- end
59
- end
60
- end
61
-
62
- def buildpack
63
- if not ENV['BUILDPACK_URL'].nil?
64
- "-b #{ENV['BUILDPACK_URL']}"
65
- else
66
- ""
67
- end
68
- end
69
-
70
- def install_launch_daemon
71
- t = ERB.new(launch_daemon_template)
72
- plist_path = File.join(release_dir, "dokuen.#{@app}.plist")
73
- File.open(plist_path, "w+") do |f|
74
- f.write(t.result(binding))
75
- end
76
- Dokuen.sys("sudo /usr/local/bin/dokuen_install_launchdaemon #{plist_path}")
77
- end
78
-
79
- def install_nginx_conf
80
- t = ERB.new(nginx_template)
81
- File.open(File.join(nginx_dir, "#{@app}.#{base_domain}.conf"), "w+") do |f|
82
- f.write(t.result(binding))
83
- end
84
- Dokuen.sys("sudo /usr/local/bin/dokuen_restart_nginx")
85
- end
86
-
87
- def base_domain
88
- ENV['BASE_DOMAIN'] || 'dokuen'
89
- end
90
-
91
- def app_name_from_env
92
- File.basename(ENV['GL_REPO']).gsub(/\.git$/, '')
93
- end
94
-
95
- def env_dir
96
- Dokuen.dir("env", @app)
97
- end
98
-
99
- def clone_dir
100
- @clone_dir ||= Dir.mktmpdir
101
- end
102
-
103
- def git_dir
104
- @git_dir || Dir.getwd
105
- end
106
-
107
- def release_dir
108
- @now = Time.now().utc().strftime("%Y%m%dT%H%M%S")
109
- @release_dir ||= File.join(Dokuen.dir('release', @app), @now)
110
- end
111
-
112
- def cache_dir
113
- Dokuen.dir('build', @app)
114
- end
115
-
116
- def nginx_dir
117
- Dokuen.dir('nginx')
118
- end
119
-
120
- def server_port
121
- ENV['USE_SSL'] ? "443" : "80"
122
- end
123
-
124
- def ssl_on
125
- ENV['USE_SSL'] ? "on" : "off"
126
- end
127
-
128
- def launch_daemon_template
129
- <<HERE
130
- <?xml version="1.0" encoding="UTF-8"?>
131
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
132
- <plist version="1.0">
133
- <dict>
134
- <key>KeepAlive</key>
135
- <true/>
136
- <key>Label</key>
137
- <string>dokuen.<%= @app %></string>
138
- <key>ProgramArguments</key>
139
- <array>
140
- <string>/usr/local/bin/dokuen</string>
141
- <string>start_app</string>
142
- <string><%= @app %></string>
143
- </array>
144
- <key>RunAtLoad</key>
145
- <true/>
146
- <key>UserName</key>
147
- <string>peter</string>
148
- <key>WorkingDirectory</key>
149
- <string>/usr/local/var/dokuen</string>
150
- <key>StandardOutPath</key>
151
- <string>/usr/local/var/dokuen/log/<%= @app %>.log</string>
152
- <key>StandardErrorPath</key>
153
- <string>/usr/local/var/dokuen/log/<%= @app %>.log</string>
154
- </dict>
155
- </plist>
156
- HERE
157
- end
158
-
159
- def nginx_template
160
- <<HERE
161
- server {
162
- server_name <%= @app %>.<%= base_domain %>;
163
- listen <%= server_port %>;
164
- ssl <%= ssl_on %>;
165
- location / {
166
- proxy_pass http://localhost:<%= ENV['PORT'] %>/;
167
- }
168
- }
169
- HERE
170
- end
171
- end
172
- end
173
-