conjure 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### Version 0.1.0
2
+ 2013-10-24
3
+
4
+ * Support revision deploys
5
+ * Add `--branch` option for deploys
6
+ * Add `console`, `log`, and `rake` commands
7
+ * SSH performance improvements
8
+
1
9
  ### Version 0.0.2
2
10
  2013-10-14
3
11
 
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
1
  ## Conjure
2
+ [![Gem Version](https://badge.fury.io/rb/conjure.png)](http://badge.fury.io/rb/conjure)
3
+ [![Build Status](https://travis-ci.org/brianauton/conjure.png?branch=master)](https://travis-ci.org/brianauton/conjure)
4
+ [![Code Climate](https://codeclimate.com/github/brianauton/conjure.png)](https://codeclimate.com/github/brianauton/conjure)
2
5
 
3
6
  Magically powerful deployment for Rails applications.
4
7
 
@@ -70,19 +73,46 @@ Finally, tell Conjure to deploy your app:
70
73
 
71
74
  The last line of the output will tell you the IP address of the
72
75
  deployed server. Repeating the command will reuse the existing server
73
- rather than deploying a new one.
76
+ rather than deploying a new one. Specify a branch to deploy with
77
+ `--branch` or `-b` (default is `master`):
78
+
79
+ conjure deploy --branch=mybranch
74
80
 
75
81
  ### Additional Commands
76
82
 
77
- These commands are available after you've deployed with `conjure
83
+ Additional commands are available after you've deployed with `conjure
78
84
  deploy`.
79
85
 
86
+ #### Export
87
+
88
+ Produce a Postgres SQL dump of the currently-deployed server's
89
+ production database, and save it to the local file `FILE`.
90
+
80
91
  conjure export FILE
81
92
 
82
- This will produce a Postgres SQL dump of the currently-deployed
83
- server's production database, and save it to the local file `FILE`.
93
+ #### Import
94
+
95
+ Overwrite the production database on the currently-deployed server
96
+ with a Postgres SQL dump from the local file `FILE`.
84
97
 
85
98
  conjure import FILE
86
99
 
87
- This will overwrite the production database on the currently-deployed
88
- server with a Postgres SQL dump from the local file `FILE`.
100
+ #### Log
101
+
102
+ Show logs from the deployed application. Optionally specify the number
103
+ of lines with `-n`, and use --tail to continue streaming new lines as
104
+ they are added to the log.
105
+
106
+ conjure log [-n=NUM] [--tail|-t]
107
+
108
+ #### Console
109
+
110
+ Open a console on the deployed application.
111
+
112
+ conjure console
113
+
114
+ #### Rake
115
+
116
+ Run a rake task on the deployed application and show the output.
117
+
118
+ conjure rake [ARGUMENTS...]
data/bin/conjure CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  require "conjure"
3
- require "thor"
4
3
 
5
4
  Conjure::Command.start
data/lib/conjure.rb CHANGED
@@ -1,7 +1,11 @@
1
1
  module Conjure
2
2
 
3
- VERSION = "0.0.2" unless defined?(VERSION)
3
+ VERSION = "0.1.0" unless defined?(VERSION)
4
4
  autoload :Command, "conjure/command"
5
+ autoload :Config, "conjure/config"
5
6
  autoload :Service, "conjure/service"
6
7
 
8
+ def self.config
9
+ @config ||= Config.load Dir.pwd
10
+ end
7
11
  end
@@ -1,44 +1,57 @@
1
+ require "thor"
2
+
1
3
  module Conjure
2
4
  class Command < Thor
3
- desc "deploy", "Deploys the app"
5
+ attr_accessor :application_options
6
+
7
+ desc "deploy", "Deploy the app"
8
+ method_option :branch, :aliases => "-b", :type => :string, :desc => "Specify branch to deploy"
9
+ method_option :test, :type => :boolean, :desc => "Describe the deploy settings but don't deploy"
10
+ method_option :origin, :type => :string, :desc => "Specify git URL to deploy from"
4
11
  def deploy
5
- Service::RailsApplication.create github_url, app_name, rails_environment, config(Dir.pwd)
12
+ self.application_options = {
13
+ :branch => options[:branch],
14
+ :test => options[:test],
15
+ :origin => options[:origin],
16
+ }
17
+ application.deploy
6
18
  end
7
19
 
8
- desc "import FILE", "Imports the production database from a postgres SQL dump"
20
+ desc "import FILE", "Import the production database from a postgres SQL dump"
9
21
  def import(file)
10
- Service::PostgresClient.create(docker_host, "#{app_name}_#{rails_environment}").import file
11
- puts "[export] #{File.size file} bytes imported from #{file}"
22
+ application.database.import file
12
23
  end
13
24
 
14
- desc "export FILE", "Exports the production database to a postgres SQL dump"
25
+ desc "export FILE", "Export the production database to a postgres SQL dump"
15
26
  def export(file)
16
- Service::PostgresClient.create(docker_host, "#{app_name}_#{rails_environment}").export file
17
- puts "[export] #{File.size file} bytes exported to #{file}"
27
+ application.database.export file
18
28
  end
19
29
 
20
- default_task :help
21
-
22
- private
23
-
24
- def rails_environment
25
- "production"
30
+ desc "log", "Display the Rails log from the deployed application"
31
+ method_option :num, :aliases => "-n", :type => :numeric, :default => 10, :desc => "Show N lines of output"
32
+ method_option :tail, :aliases => "-t", :type => :boolean, :desc => "Continue streaming new log entries"
33
+ def log
34
+ application.rails.log :lines => options[:num], :tail => options[:tail]
26
35
  end
27
36
 
28
- def docker_host
29
- Service::DockerHost.create "#{app_name}-#{rails_environment}", config(Dir.pwd)
37
+ desc "rake [ARGUMENTS...]", "Run the specified rake task on the deployed application"
38
+ def rake(*arguments)
39
+ application.rails.rake arguments.join(" ")
30
40
  end
31
41
 
32
- def config(source_path)
33
- require "ostruct"
34
- config_path = File.join source_path, "config", "conjure.yml"
35
- data = YAML.load_file config_path
36
- data["config_path"] = File.dirname config_path
37
- OpenStruct.new data
42
+ desc "console", "Start a console on the deployed application"
43
+ def console
44
+ application.rails.console
38
45
  end
39
46
 
40
- def app_name
41
- name_from_github_url github_url
47
+ default_task :help
48
+
49
+ private
50
+
51
+ def application
52
+ self.application_options ||= {}
53
+ self.application_options[:origin] ||= github_url
54
+ Service::RailsApplication.create self.application_options
42
55
  end
43
56
 
44
57
  def github_url
@@ -49,9 +62,5 @@ module Conjure
49
62
  remote_info = `cd #{source_path}; git remote -v |grep origin`
50
63
  remote_info.match(/(git@github.com[^ ]+)/)[1]
51
64
  end
52
-
53
- def name_from_github_url(github_url)
54
- github_url.match(/\/([^.]+)\.git$/)[1]
55
- end
56
65
  end
57
66
  end
@@ -0,0 +1,11 @@
1
+ module Conjure
2
+ class Config
3
+ def self.load(root_path)
4
+ require "ostruct"
5
+ config_path = File.join root_path, "config", "conjure.yml"
6
+ data = YAML.load_file config_path
7
+ data["config_path"] = File.dirname config_path
8
+ OpenStruct.new data
9
+ end
10
+ end
11
+ end
@@ -1,17 +1,23 @@
1
1
  module Conjure
2
2
  module Service
3
3
  autoload :RailsApplication, "conjure/service/rails_application"
4
+ autoload :RailsCodebase, "conjure/service/rails_codebase"
4
5
  autoload :RailsServer, "conjure/service/rails_server"
5
6
  autoload :MachineInstance, "conjure/service/machine_instance"
6
7
  autoload :DockerHost, "conjure/service/docker_host"
7
8
  autoload :CloudServer, "conjure/service/cloud_server"
8
- autoload :PostgresServer, "conjure/service/postgres_server"
9
- autoload :PostgresClient, "conjure/service/postgres_client"
9
+ autoload :PostgresDatabase, "conjure/service/postgres_database"
10
+ autoload :RemoteShell, "conjure/service/remote_shell"
10
11
  end
11
12
 
12
13
  class Basic
13
14
  def self.create(*args)
14
15
  new(*args)
15
16
  end
17
+
18
+ def file_contents(file_path)
19
+ file_path = File.join Conjure.config.config_path, file_path
20
+ `cat #{file_path}`
21
+ end
16
22
  end
17
23
  end
@@ -3,15 +3,14 @@ module Conjure
3
3
  class CloudServer < Basic
4
4
  require "fog"
5
5
 
6
- def initialize(name, config = {})
6
+ def initialize(name)
7
7
  @name = name
8
- @config = config
9
8
  end
10
9
 
11
- def run(command, options = {})
10
+ def run(command, options = {}, &block)
12
11
  set_fog_credentials
13
12
  upload_files options[:files].to_a
14
- result = server.ssh(command).first
13
+ result = remote_shell.run(command, :stream_stdin => options[:stream_stdin], &block)
15
14
  remove_files options[:files].to_a
16
15
  result
17
16
  end
@@ -23,7 +22,7 @@ module Conjure
23
22
  end
24
23
 
25
24
  def remove_files(files)
26
- files.each{|local_path, remote_path| server.ssh "rm -f #{remote_path}"}
25
+ files.each{|local_path, remote_path| run "rm -f #{remote_path}"}
27
26
  end
28
27
 
29
28
  def server
@@ -64,8 +63,8 @@ module Conjure
64
63
  def compute_options
65
64
  {
66
65
  provider: :digitalocean,
67
- digitalocean_api_key: config.digitalocean_api_key,
68
- digitalocean_client_id: config.digitalocean_client_id,
66
+ digitalocean_api_key: Conjure.config.digitalocean_api_key,
67
+ digitalocean_client_id: Conjure.config.digitalocean_client_id,
69
68
  }
70
69
  end
71
70
 
@@ -74,27 +73,23 @@ module Conjure
74
73
  end
75
74
 
76
75
  def region_id
77
- @region_id ||= connection.regions.find{|r| r.name == config.digitalocean_region}.id
76
+ @region_id ||= connection.regions.find{|r| r.name == Conjure.config.digitalocean_region}.id
78
77
  end
79
78
 
80
79
  def image_id
81
80
  @image_id ||= connection.images.find{|r| r.name == "Ubuntu 13.04 x64"}.id
82
81
  end
83
82
 
84
- def config
85
- @config
86
- end
87
-
88
83
  def set_fog_credentials
89
84
  Fog.credentials.merge! fog_credentials
90
85
  end
91
86
 
92
87
  def private_key_file
93
- Pathname.new(config.config_path).join config.private_key_file
88
+ Pathname.new(Conjure.config.config_path).join Conjure.config.private_key_file
94
89
  end
95
90
 
96
91
  def public_key_file
97
- Pathname.new(config.config_path).join config.public_key_file
92
+ Pathname.new(Conjure.config.config_path).join Conjure.config.public_key_file
98
93
  end
99
94
 
100
95
  def fog_credentials
@@ -103,6 +98,14 @@ module Conjure
103
98
  public_key_path: public_key_file,
104
99
  }
105
100
  end
101
+
102
+ def remote_shell
103
+ @remote_shell ||= RemoteShell.new(
104
+ :ip_address => server.public_ip_address,
105
+ :username => "root",
106
+ :private_key_path => private_key_file,
107
+ )
108
+ end
106
109
  end
107
110
  end
108
111
  end
@@ -3,17 +3,12 @@ module Conjure
3
3
  class DockerHost < Basic
4
4
  VERBOSE = false
5
5
 
6
- def initialize(server_name, config = {})
6
+ def initialize(server_name)
7
7
  @server_name = server_name
8
- @config = config
9
8
  end
10
9
 
11
10
  def server
12
- @server ||= Service::CloudServer.create @server_name, config
13
- end
14
-
15
- def config
16
- @config
11
+ @server ||= Service::CloudServer.create @server_name
17
12
  end
18
13
 
19
14
  def ip_address
@@ -22,6 +17,8 @@ module Conjure
22
17
 
23
18
  def new_docker_path
24
19
  puts "[docker] Installing docker"
20
+ server.run "dd if=/dev/zero of=/root/swapfile bs=1024 count=524288"
21
+ server.run "mkswap /root/swapfile; swapon /root/swapfile"
25
22
  server.run "curl https://get.docker.io/gpg | apt-key add -"
26
23
  server.run "echo 'deb https://get.docker.io/ubuntu docker main' >/etc/apt/sources.list.d/docker.list"
27
24
  server.run "apt-get update"
@@ -41,47 +38,52 @@ module Conjure
41
38
  @docker_path ||= new_docker_path
42
39
  end
43
40
 
44
- def command(command, options = {})
41
+ def command(command, options = {}, &block)
45
42
  full_command = "#{docker_path} #{command}"
46
43
  full_command = "nohup #{full_command}" if options[:nohup]
47
44
  full_command = "echo '#{shell_escape options[:stdin]}' | #{full_command}" if options[:stdin]
48
45
  puts " [scp] #{options[:files].inspect}" if VERBOSE and options[:files]
49
46
  puts " [ssh] #{full_command}" if VERBOSE
50
- result = server.run full_command, files: options[:files]
47
+ result = server.run full_command, :stream_stdin => options[:stream_stdin], :files => options[:files], &block
51
48
  raise "Docker error: #{result.stdout} #{result.stderr}" unless result.status == 0
52
49
  result.stdout
53
50
  end
54
51
 
55
- def clean_stopped_processes
56
- command "rm `#{docker_path} ps -a -q`"
52
+ def ensure_host_directory(dir)
53
+ server.run "mkdir -p #{dir}"
57
54
  end
58
55
 
59
56
  def shell_escape(text)
60
57
  text.gsub "'", "'\"'\"'"
61
58
  end
62
59
 
60
+ def images
61
+ ImageSet.new self
62
+ end
63
+
63
64
  def containers
64
- ContainerSet.new self
65
+ ContainerSet.new :host => self
65
66
  end
66
67
  end
67
68
 
68
- class ContainerSet
69
+ class ImageSet
69
70
  def initialize(host)
70
71
  @host = host
71
72
  end
72
73
 
73
74
  def create(options)
74
- Container.new @host, options
75
+ Image.new @host, options
75
76
  end
76
77
  end
77
78
 
78
- class Container
79
+ class Image
79
80
  def initialize(host, options)
80
81
  @host = host
81
82
  @label = options[:label]
82
83
  @base_image = options[:base_image]
83
84
  @ports = options[:ports].to_a
84
85
  @volumes = options[:volumes].to_a
86
+ @host_volumes = options[:host_volumes]
85
87
  @setup_commands = options[:setup_commands].to_a
86
88
  @daemon_command = options[:daemon_command]
87
89
  @environment = options[:environment]
@@ -89,25 +91,35 @@ module Conjure
89
91
  end
90
92
 
91
93
  def image_fingerprint
92
- {base_image: @base_image, setup_commands: @setup_commands}
94
+ Digest::SHA1.hexdigest(dockerfile)[0..11]
95
+ end
96
+
97
+ def expected_image_name
98
+ "#{@label}:#{image_fingerprint}"
99
+ end
100
+
101
+ def installed_image_name
102
+ build unless image_installed?
103
+ expected_image_name
93
104
  end
94
105
 
95
- def image_name
96
- hash = Digest::SHA1.hexdigest(image_fingerprint.to_yaml).first(12)
97
- "#{@label}_#{hash}"
106
+ def image_installed?
107
+ @host.command("history #{expected_image_name}") rescue false
98
108
  end
99
109
 
100
- def run
101
- unless id
102
- build
103
- puts "[docker] Starting #{@label} image"
104
- container_id = @host.command("run -d #{@label}").strip
105
- if(!id)
110
+ def run(command = "")
111
+ unless running_container
112
+ puts "[docker] Starting #{@label}"
113
+ run_options = @host_volumes ? host_volume_options(@host_volumes) : ""
114
+ command = shell_command command if command != ""
115
+ container_id = @host.command("run #{run_options} -d #{installed_image_name} #{command}").strip
116
+ if(!running_container)
106
117
  output = @host.command "logs #{container_id}"
107
118
  raise "Docker: #{@label} daemon exited with: #{output}"
108
119
  end
109
120
  end
110
- puts "[docker] #{@label} is running at #{ip_address}"
121
+ puts "[docker] #{@label} is running at #{running_container.ip_address}"
122
+ running_container
111
123
  end
112
124
 
113
125
  def raise_build_errors(build_output)
@@ -121,11 +133,8 @@ module Conjure
121
133
  end
122
134
  end
123
135
 
124
- def raise_run_errors(run_output)
125
- end
126
-
127
136
  def dockerfile
128
- lines = ["FROM #{@base_image}"]
137
+ lines = ["FROM #{base_image_name}"]
129
138
  lines += @environment.map{|k, v| "ENV #{k} #{v}"} if @environment
130
139
  lines += @setup_commands.map{|c| "RUN #{c}"}
131
140
  lines << "EXPOSE #{@ports.map{|p| "#{p}:#{p}"}.join ' '}" if @ports.to_a.any?
@@ -134,17 +143,30 @@ module Conjure
134
143
  lines.join "\n"
135
144
  end
136
145
 
146
+ def base_image_name
147
+ @base_image.respond_to?(:installed_image_name) ? @base_image.installed_image_name : @base_image
148
+ end
149
+
137
150
  def build
151
+ destroy_instances
138
152
  puts "[docker] Building #{@label} image"
139
- raise_build_errors(@host.command "build -t #{@label} -", stdin: dockerfile)
140
- @host.clean_stopped_processes
153
+ raise_build_errors(@host.command "build -t #{expected_image_name} -", stdin: dockerfile)
154
+ @host.containers.destroy_all_stopped
141
155
  end
142
156
 
143
- def command(command, options = {})
144
- build
145
- puts "[docker] Executing #{@label} image"
157
+ def command(command, options = {}, &block)
158
+ destroy_instances
146
159
  file_options = options[:files] ? "-v /files:/files" : ""
147
- @host.command "run #{file_options} #{@label} #{command}", files: files_hash(options[:files])
160
+ file_options += " "+host_volume_options(@host_volumes) if @host_volumes
161
+ file_options += " -i" if options[:stream_stdin]
162
+ @host.command "run #{file_options} #{installed_image_name} #{shell_command command}", :stream_stdin => options[:stream_stdin], :files => files_hash(options[:files]), &block
163
+ end
164
+
165
+ def host_volume_options(host_volumes)
166
+ host_volumes.map do |host_path, container_path|
167
+ @host.ensure_host_directory host_path
168
+ "-v=#{host_path}:#{container_path}:rw"
169
+ end.join " "
148
170
  end
149
171
 
150
172
  def files_hash(files_array)
@@ -157,10 +179,52 @@ module Conjure
157
179
  "bash -c '#{@host.shell_escape command}'"
158
180
  end
159
181
 
160
- def id
161
- @id ||= @host.command("ps | grep #{@label}: ; true").strip.split("\n").first.to_s[0..11]
162
- @id = nil if @id == ""
163
- @id
182
+ def running_container
183
+ @runnning_container ||= @host.containers.find(:image_name => expected_image_name)
184
+ end
185
+
186
+ def destroy_instances
187
+ @host.containers.destroy_all :image_name => @label
188
+ end
189
+
190
+ def stop
191
+ destroy_instances
192
+ end
193
+ end
194
+
195
+ class ContainerSet
196
+ attr_accessor :host
197
+
198
+ def initialize(options)
199
+ self.host = options[:host]
200
+ end
201
+
202
+ def find(options)
203
+ image_name = options[:image_name]
204
+ id = host.command("ps | grep #{image_name} ; true").strip.split("\n").first.to_s[0..11]
205
+ id = nil if id == ""
206
+ Container.new(:host => host, :id => id) if id
207
+ end
208
+
209
+ def destroy_all_stopped
210
+ host.command "rm `#{host.docker_path} ps -a -q`"
211
+ end
212
+
213
+ def destroy_all(options)
214
+ while container = find(:image_name => options[:image_name]) do
215
+ puts "[docker] Stopping #{options[:image_name]}"
216
+ host.command "stop #{container.id}"
217
+ host.command "rm #{container.id}"
218
+ end
219
+ end
220
+ end
221
+
222
+ class Container
223
+ attr_accessor :id, :host
224
+
225
+ def initialize(options)
226
+ self.id = options[:id]
227
+ self.host = options[:host]
164
228
  end
165
229
 
166
230
  def ip_address
@@ -169,7 +233,7 @@ module Conjure
169
233
 
170
234
  def status
171
235
  require "json"
172
- JSON.parse(@host.command "inspect #{id}").first
236
+ JSON.parse(host.command "inspect #{id}").first
173
237
  end
174
238
  end
175
239
  end
@@ -0,0 +1,69 @@
1
+ module Conjure
2
+ module Service
3
+ class PostgresDatabase < Basic
4
+ def initialize(host, db_name)
5
+ @host = host
6
+ @db_name = db_name
7
+ end
8
+
9
+ def base_image
10
+ @base_image ||= @host.images.create(
11
+ label: "postgres",
12
+ base_image: "ubuntu",
13
+ setup_commands: [
14
+ "apt-get install -y python-software-properties software-properties-common",
15
+ "add-apt-repository -y ppa:pitti/postgresql",
16
+ "apt-get update",
17
+ "apt-get install -y postgresql-9.2 postgresql-client-9.2 postgresql-contrib-9.2",
18
+ ],
19
+ )
20
+ end
21
+
22
+ def server_image
23
+ @server_image ||= @host.images.create(
24
+ label: "pgserver",
25
+ base_image: base_image,
26
+ setup_commands: [
27
+ "service postgresql start; su postgres -c 'createuser -d -r -s root; createdb -O root root'; service postgresql stop",
28
+ "echo 'host all all 0.0.0.0/0 trust' >>/etc/postgresql/9.2/main/pg_hba.conf",
29
+ "echo \"listen_addresses='*'\" >>/etc/postgresql/9.2/main/postgresql.conf",
30
+ ],
31
+ daemon_command: "su postgres -c '#{bin_path}/postgres -c config_file=/etc/postgresql/9.2/main/postgresql.conf'",
32
+ volumes: ["/var/lib/postgresql/9.2/main"],
33
+ )
34
+ end
35
+
36
+ def run
37
+ container
38
+ end
39
+
40
+ def container
41
+ @container ||= server_image.run
42
+ end
43
+
44
+ def ip_address
45
+ container.ip_address
46
+ end
47
+
48
+ def export(file)
49
+ File.open file, "w" do |f|
50
+ f.write base_image.command("#{bin_path}/pg_dump #{client_options} #{@db_name}")
51
+ end
52
+ puts "[export] #{File.size file} bytes exported to #{file}"
53
+ end
54
+
55
+ def import(file)
56
+ base_image.command "#{bin_path}/psql #{client_options} -d #{@db_name} -f /files/#{File.basename file}", files: [file]
57
+ puts "[import] #{File.size file} bytes imported from #{file}"
58
+ end
59
+
60
+ def client_options
61
+ "-U root -h #{ip_address}"
62
+ end
63
+
64
+ def bin_path
65
+ "/usr/lib/postgresql/9.2/bin"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,15 +1,42 @@
1
1
  module Conjure
2
2
  module Service
3
3
  class RailsApplication < Basic
4
- def initialize(github_url, name = "myapp", environment = "production", config = {})
5
- @name = name
6
- @environment = environment
7
- docker = Service::DockerHost.create "#{name}-#{environment}", config
8
- postgres = Service::PostgresServer.create docker
9
- postgres.run
10
- rails = Service::RailsServer.create docker, github_url, name, postgres.ip_address, environment
11
- rails.run
12
- puts "[deploy] Application deployed to #{docker.ip_address}"
4
+ def initialize(options)
5
+ @origin = options[:origin]
6
+ @name = name_from_origin @origin
7
+ @branch = options[:branch] || "master"
8
+ @environment = "production"
9
+ @test = options[:test]
10
+ end
11
+
12
+ def deploy
13
+ puts "[deploy] Deploying #{@name}:#{@branch} to #{@environment}"
14
+ unless @test
15
+ database.run
16
+ codebase.install
17
+ rails.run
18
+ puts "[deploy] Application deployed to #{docker.ip_address}"
19
+ end
20
+ end
21
+
22
+ def docker
23
+ @docker ||= Service::DockerHost.create "#{@name}-#{@environment}"
24
+ end
25
+
26
+ def database
27
+ @database ||= Service::PostgresDatabase.create docker, "#{@name}_#{@environment}"
28
+ end
29
+
30
+ def codebase
31
+ @codebase ||= Service::RailsCodebase.create docker, @origin, @branch, @name, database.ip_address, @environment
32
+ end
33
+
34
+ def rails
35
+ @rails ||= Service::RailsServer.create docker, @name, @environment
36
+ end
37
+
38
+ def name_from_origin(origin)
39
+ origin.match(/\/([^.]+)\.git$/)[1]
13
40
  end
14
41
  end
15
42
  end
@@ -0,0 +1,71 @@
1
+ module Conjure
2
+ module Service
3
+ class RailsCodebase < Basic
4
+ def initialize(host, github_url, branch, app_name, database_ip_address, rails_environment)
5
+ @github_url = github_url
6
+ @branch = branch
7
+ @app_name = app_name
8
+ @database_ip_address = database_ip_address
9
+ @rails_environment = rails_environment
10
+ github_private_key = file_contents(Conjure.config.private_key_file).gsub("\n", "\\n")
11
+ github_public_key = file_contents(Conjure.config.public_key_file).gsub("\n", "\\n")
12
+ @image = host.images.create(
13
+ label: "codebase",
14
+ base_image: "ubuntu",
15
+ setup_commands: [
16
+ "apt-get install -y git",
17
+ "mkdir -p /root/.ssh; echo '#{github_private_key}' > /root/.ssh/id_rsa",
18
+ "mkdir -p /root/.ssh; echo '#{github_public_key}' > /root/.ssh/id_rsa.pub",
19
+ "chmod -R go-rwx /root/.ssh",
20
+ "echo 'Host github.com\\n\\tStrictHostKeyChecking no\\n' >> /root/.ssh/config",
21
+ ],
22
+ host_volumes: {"/rails_app" => "/#{app_name}"},
23
+ )
24
+ end
25
+
26
+ def database_yml
27
+ {
28
+ @rails_environment => {
29
+ "adapter" => "postgresql",
30
+ "database" => "#{@app_name}_#{@rails_environment}",
31
+ "encoding" => "utf8",
32
+ "host" => @database_ip_address,
33
+ "username" => "root",
34
+ "template" => "template0",
35
+ }
36
+ }.to_yaml
37
+ end
38
+
39
+ def install
40
+ code_checked_out ? fetch_code_updates : checkout_code
41
+ configure_database
42
+ configure_logs
43
+ end
44
+
45
+ def code_checked_out
46
+ @image.command("[ -d #{@app_name}/.git ] && echo yes; true").strip == "yes"
47
+ end
48
+
49
+ def checkout_code
50
+ puts "[ repo] Checking out code from git"
51
+ @image.command "git clone -b #{@branch} #{@github_url}"
52
+ end
53
+
54
+ def fetch_code_updates
55
+ puts "[ repo] Fetching code updates from git"
56
+ @image.command "cd #{@app_name}; git reset --hard; git checkout #{@branch}; git pull"
57
+ end
58
+
59
+ def configure_database
60
+ puts "[ repo] Generating database.yml"
61
+ @image.command "echo '#{database_yml}' >/#{@app_name}/config/database.yml"
62
+ end
63
+
64
+ def configure_logs
65
+ puts "[ repo] Configuring application logger"
66
+ setup = 'Rails.logger = Logger.new "#{Rails.root}/log/#{Rails.env}.log"'
67
+ @image.command "echo '#{setup}' >/#{@app_name}/config/initializers/z_conjure_logger.rb"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,68 +1,111 @@
1
1
  module Conjure
2
2
  module Service
3
3
  class RailsServer < Basic
4
- def file_contents(config, file_path)
5
- file_path = File.join config.config_path, file_path
6
- `cat #{file_path}`
4
+ def initialize(host, app_name, rails_environment)
5
+ @host = host
6
+ @app_name = app_name
7
+ @rails_environment = rails_environment
7
8
  end
8
9
 
9
- def initialize(host, github_url, app_name, database_ip_address, rails_environment = "production")
10
- config = host.config
11
- ruby_version = file_contents(config, "../.ruby-version").strip
12
- github_private_key = file_contents(config, config.private_key_file).gsub("\n", "\\n")
13
- github_public_key = file_contents(config, config.public_key_file).gsub("\n", "\\n")
14
- @container = host.containers.create(
15
- label: "rails",
10
+ def base_image
11
+ @base_image ||= @host.images.create(
12
+ label: "rails_base",
16
13
  base_image: "ubuntu",
17
14
  setup_commands: [
18
- "apt-get install -y curl git",
15
+ "apt-get install -y curl git",
19
16
  "curl -L https://get.rvm.io | bash -s stable",
20
17
  "rvm install #{ruby_version}",
21
18
  "bash -c 'source /usr/local/rvm/scripts/rvm; rvm use #{ruby_version}@global --default'",
22
- "mkdir -p /root/.ssh; echo '#{github_private_key}' > /root/.ssh/id_rsa",
23
- "mkdir -p /root/.ssh; echo '#{github_public_key}' > /root/.ssh/id_rsa.pub",
24
- "chmod -R go-rwx /root/.ssh",
25
- "echo 'Host github.com\\n\\tStrictHostKeyChecking no\\n' >> /root/.ssh/config",
26
- "git clone #{github_url}",
27
19
  "apt-get install -y #{apt_packages_required_for_gems.join ' '}",
28
- "cd #{app_name}; bundle --deployment",
29
-
30
20
  "echo 'deb http://us.archive.ubuntu.com/ubuntu/ precise universe' >>/etc/apt/sources.list",
31
21
  "apt-get install -y python-software-properties software-properties-common",
32
22
  "add-apt-repository -y ppa:chris-lea/node.js-legacy",
33
23
  "apt-get update",
34
24
  "apt-get install -y nodejs",
35
25
 
36
- "echo '#{database_yml database_ip_address, app_name, rails_environment}' >/#{app_name}/config/database.yml",
37
- "cd #{app_name}; bundle exec rake db:setup",
38
26
  ],
39
27
  environment: {
40
28
  PATH:"/usr/local/rvm/gems/ruby-1.9.3-p448@global/bin:/usr/local/rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
41
- RAILS_ENV: rails_environment,
29
+ RAILS_ENV: @rails_environment,
42
30
  GITHUB_TOKEN: ENV["GITHUB_TOKEN"],
43
31
  FRECKLE_SUBDOMAIN: "neomind",
44
32
  },
45
- daemon_command: "cd #{app_name}; bundle exec rails server -p 80",
33
+ host_volumes: {"/rails_app" => "/#{@app_name}"},
34
+ )
35
+ end
36
+
37
+ def server_image
38
+ @server_image ||= @host.images.create(
39
+ label: "rails_server",
40
+ base_image: base_image,
46
41
  ports: [80],
47
- volumes: ["/#{app_name}/log"],
42
+ environment: {
43
+ PATH:"/usr/local/rvm/gems/ruby-1.9.3-p448@global/bin:/usr/local/rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
44
+ },
45
+ host_volumes: {"/rails_app" => "/#{@app_name}"},
48
46
  )
49
47
  end
50
48
 
51
49
  def run
52
- @container.run
50
+ install_gems
51
+ update_database
52
+ restart_server
53
+ end
54
+
55
+ def install_gems
56
+ puts "[ rails] Installing gems"
57
+ base_image.command "cd #{@app_name}; bundle --deployment"
58
+ end
59
+
60
+ def update_database
61
+ database_exists ? migrate_database : initialize_database
62
+ end
63
+
64
+ def database_exists
65
+ puts "[ rails] Checking the database status"
66
+ base_image.command("cd #{@app_name}; bundle exec rake db:version; true").include? "Current version:"
67
+ end
68
+
69
+ def migrate_database
70
+ puts "[ rails] Migrating the database"
71
+ base_image.command "cd #{@app_name}; bundle exec rake db:migrate"
72
+ end
73
+
74
+ def initialize_database
75
+ puts "[ rails] Setting up the database"
76
+ base_image.command "cd #{@app_name}; bundle exec rake db:setup"
77
+ end
78
+
79
+ def restart_server
80
+ server_image.stop
81
+ server_image.run "cd #{@app_name}; rm -f tmp/pids/server.pid; bundle exec rails server -p 80"
82
+ end
83
+
84
+ def log(options = {})
85
+ arguments = []
86
+ arguments << "-n #{options[:lines]}" if options[:lines]
87
+ arguments << "-f" if options[:tail]
88
+ log_file = "#{@app_name}/log/#{@rails_environment}.log"
89
+ base_image.command "tail #{arguments.join ' '} #{log_file}" do |stdout, stderr|
90
+ puts stdout
91
+ end
92
+ rescue Interrupt => e
93
+ end
94
+
95
+ def rake(command)
96
+ base_image.command "cd #{@app_name}; bundle exec rake #{command}" do |stdout, stderr|
97
+ print stdout
98
+ end
99
+ end
100
+
101
+ def console
102
+ base_image.command "cd #{@app_name}; bundle exec rails console", :stream_stdin => true do |stdout, stderr|
103
+ print stdout
104
+ end
53
105
  end
54
106
 
55
- def database_yml(database_ip_address, app_name, rails_environment)
56
- {
57
- rails_environment => {
58
- "adapter" => "postgresql",
59
- "database" => "#{app_name}_#{rails_environment}",
60
- "encoding" => "utf8",
61
- "host" => database_ip_address,
62
- "username" => "root",
63
- "template" => "template0",
64
- }
65
- }.to_yaml.gsub("\n", "\\n")
107
+ def ruby_version
108
+ file_contents("../.ruby-version").strip
66
109
  end
67
110
 
68
111
  def apt_packages_required_for_gems
@@ -0,0 +1,61 @@
1
+ module Conjure
2
+ module Service
3
+ class RemoteShell
4
+ require "net/ssh"
5
+
6
+ def initialize(options)
7
+ @options = options
8
+ end
9
+
10
+ def run(command, options = {})
11
+ stdout, stderr, exit_status = ["", "", nil]
12
+ session.open_channel do |channel|
13
+ channel.request_pty
14
+ channel.exec command do |c, success|
15
+ raise "Failed to execute command via SSH" unless success
16
+ channel.on_data do |c, data|
17
+ yield data if block_given?
18
+ stdout << data
19
+ end
20
+ channel.on_extended_data { |c, type, data| stderr << data }
21
+ channel.on_request("exit-status") { |c, data| exit_status = data.read_long }
22
+ end
23
+ if options[:stream_stdin]
24
+ channel.on_process do
25
+ poll_stream(STDIN) { |data| channel.send_data data }
26
+ end
27
+ end
28
+ end
29
+ if options[:stream_stdin]
30
+ with_raw_tty { session.loop 0.01 }
31
+ else
32
+ session.loop
33
+ end
34
+ Result.new stdout, stderr, exit_status
35
+ end
36
+
37
+ def session
38
+ session_options = {
39
+ :auth_methods => ["publickey"],
40
+ :key_data => File.read(@options[:private_key_path]),
41
+ :keys_only => true,
42
+ :paranoid => false,
43
+ }
44
+ @session ||= Net::SSH.start @options[:ip_address], @options[:username], session_options
45
+ end
46
+
47
+ def poll_stream(stream, &block)
48
+ yield stream.sysread(1) if IO.select([stream], nil, nil, 0.01)
49
+ end
50
+
51
+ def with_raw_tty
52
+ system "stty raw -echo"
53
+ yield
54
+ ensure
55
+ system "stty -raw echo"
56
+ end
57
+ end
58
+
59
+ Result = Struct.new(:stdout, :stderr, :status)
60
+ end
61
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conjure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
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: 2013-10-15 00:00:00.000000000 Z
12
+ date: 2013-10-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fog
@@ -59,6 +59,38 @@ dependencies:
59
59
  - - ! '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: minitest
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
62
94
  description:
63
95
  email:
64
96
  - brianauton@gmail.com
@@ -70,11 +102,13 @@ files:
70
102
  - lib/conjure.rb
71
103
  - lib/conjure/service/cloud_server.rb
72
104
  - lib/conjure/service/rails_server.rb
73
- - lib/conjure/service/postgres_server.rb
105
+ - lib/conjure/service/rails_codebase.rb
74
106
  - lib/conjure/service/rails_application.rb
75
107
  - lib/conjure/service/machine_instance.rb
76
- - lib/conjure/service/postgres_client.rb
108
+ - lib/conjure/service/remote_shell.rb
77
109
  - lib/conjure/service/docker_host.rb
110
+ - lib/conjure/service/postgres_database.rb
111
+ - lib/conjure/config.rb
78
112
  - lib/conjure/service.rb
79
113
  - lib/conjure/command.rb
80
114
  - README.md
@@ -1,32 +0,0 @@
1
- module Conjure
2
- module Service
3
- class PostgresClient < Basic
4
- def initialize(host, db_name)
5
- server = PostgresServer.create host
6
- server.run
7
- @server_ip = server.ip_address
8
- @db_name = db_name
9
- @container = host.containers.create(
10
- label: "pgclient",
11
- base_image: "ubuntu",
12
- setup_commands: [
13
- "apt-get install -y python-software-properties software-properties-common",
14
- "add-apt-repository -y ppa:pitti/postgresql",
15
- "apt-get update",
16
- "apt-get install -y postgresql-9.2 postgresql-client-9.2 postgresql-contrib-9.2",
17
- ],
18
- )
19
- end
20
-
21
- def export(file)
22
- File.open file, "w" do |f|
23
- f.write @container.command("/usr/lib/postgresql/9.2/bin/pg_dump -U root -h #{@server_ip} #{@db_name}")
24
- end
25
- end
26
-
27
- def import(file)
28
- @container.command "/usr/lib/postgresql/9.2/bin/psql -U root -h #{@server_ip} -d #{@db_name} -f /files/#{File.basename file}", files: [file]
29
- end
30
- end
31
- end
32
- end
@@ -1,31 +0,0 @@
1
- module Conjure
2
- module Service
3
- class PostgresServer < Basic
4
- def initialize(host)
5
- @container = host.containers.create(
6
- label: "postgres",
7
- base_image: "ubuntu",
8
- setup_commands: [
9
- "apt-get install -y python-software-properties software-properties-common",
10
- "add-apt-repository -y ppa:pitti/postgresql",
11
- "apt-get update",
12
- "apt-get install -y postgresql-9.2 postgresql-client-9.2 postgresql-contrib-9.2",
13
- "service postgresql start; su postgres -c 'createuser -d -r -s root; createdb -O root root'; service postgresql stop",
14
- "echo 'host all all 0.0.0.0/0 trust' >>/etc/postgresql/9.2/main/pg_hba.conf",
15
- "echo \"listen_addresses='*'\" >>/etc/postgresql/9.2/main/postgresql.conf",
16
- ],
17
- daemon_command: "su postgres -c '/usr/lib/postgresql/9.2/bin/postgres -c config_file=/etc/postgresql/9.2/main/postgresql.conf'",
18
- volumes: ["/var/lib/postgresql/9.2/main"],
19
- )
20
- end
21
-
22
- def run
23
- @container.run
24
- end
25
-
26
- def ip_address
27
- @container.ip_address
28
- end
29
- end
30
- end
31
- end