conjure 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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