conjure 0.2.10 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/History.md +8 -0
  2. data/README.md +35 -76
  3. data/lib/conjure.rb +1 -12
  4. data/lib/conjure/delayed_job.rb +39 -0
  5. data/lib/conjure/digital_ocean/droplet.rb +5 -2
  6. data/lib/conjure/docker/host.rb +75 -0
  7. data/lib/conjure/docker/template.rb +71 -0
  8. data/lib/conjure/instance.rb +31 -76
  9. data/lib/conjure/passenger.rb +123 -0
  10. data/lib/conjure/postgres.rb +67 -0
  11. data/lib/conjure/rails_application.rb +32 -0
  12. data/lib/conjure/server.rb +41 -0
  13. data/lib/conjure/swap.rb +28 -0
  14. data/lib/conjure/{provision/templates → templates}/application-no-ssl.conf.erb +2 -1
  15. data/lib/conjure/{provision/templates → templates}/application-ssl.conf.erb +2 -1
  16. data/lib/conjure/version.rb +1 -1
  17. metadata +12 -41
  18. data/lib/conjure/application.rb +0 -35
  19. data/lib/conjure/command.rb +0 -74
  20. data/lib/conjure/command_target.rb +0 -25
  21. data/lib/conjure/config.rb +0 -44
  22. data/lib/conjure/data_set.rb +0 -7
  23. data/lib/conjure/identity.rb +0 -25
  24. data/lib/conjure/log.rb +0 -26
  25. data/lib/conjure/provider.rb +0 -26
  26. data/lib/conjure/provision.rb +0 -1
  27. data/lib/conjure/provision/docker/host.rb +0 -32
  28. data/lib/conjure/provision/docker/image.rb +0 -55
  29. data/lib/conjure/provision/docker/template.rb +0 -55
  30. data/lib/conjure/provision/instance.rb +0 -52
  31. data/lib/conjure/provision/local_docker.rb +0 -16
  32. data/lib/conjure/provision/passenger.rb +0 -111
  33. data/lib/conjure/provision/postgres.rb +0 -70
  34. data/lib/conjure/provision/server.rb +0 -78
  35. data/lib/conjure/service/cloud_server.rb +0 -112
  36. data/lib/conjure/service/database.rb +0 -25
  37. data/lib/conjure/service/database/mysql.rb +0 -69
  38. data/lib/conjure/service/database/postgres.rb +0 -77
  39. data/lib/conjure/service/digital_ocean_account.rb +0 -31
  40. data/lib/conjure/service/docker_host.rb +0 -259
  41. data/lib/conjure/service/docker_shell.rb +0 -46
  42. data/lib/conjure/service/forwarded_shell.rb +0 -25
  43. data/lib/conjure/service/rails_codebase.rb +0 -67
  44. data/lib/conjure/service/rails_console.rb +0 -10
  45. data/lib/conjure/service/rails_log_view.rb +0 -14
  46. data/lib/conjure/service/rails_server.rb +0 -91
  47. data/lib/conjure/service/rake_task.rb +0 -11
  48. data/lib/conjure/service/remote_file_set.rb +0 -24
  49. data/lib/conjure/service/remote_shell.rb +0 -73
  50. data/lib/conjure/service/repository_link.rb +0 -52
  51. data/lib/conjure/service/volume.rb +0 -28
  52. data/lib/conjure/target.rb +0 -19
  53. data/lib/conjure/view/application_view.rb +0 -42
  54. data/lib/conjure/view/table_view.rb +0 -38
data/History.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### Version 0.3.0
2
+ 2016-01-04
3
+
4
+ * Remove command-line app in favor of the provisioning API
5
+ * Revise API with support for updating existing Instances
6
+ * Monitor and restart background workers for deployed apps using DelayedJob
7
+ * Upgrade to Passenger base images v0.9.18
8
+
1
9
  ### Version 0.2.10
2
10
  2015-06-24
3
11
 
data/README.md CHANGED
@@ -4,36 +4,27 @@
4
4
  [![Code Climate](https://codeclimate.com/github/brianauton/conjure.png)](https://codeclimate.com/github/brianauton/conjure)
5
5
  [![Dependency Status](https://gemnasium.com/brianauton/conjure.png)](https://gemnasium.com/brianauton/conjure)
6
6
 
7
+ Conjure is a Ruby library for creating and updating cloud server instances as part of a Rails deployment workflow.
7
8
 
8
- Magically powerful deployment for Rails applications.
9
+ Conjure creates a cloud server instance, and uses Docker to start separate containers for the database and the web server. It sets up data volume containers for both of these service containers, so all data will be preserved if any individual containers or the server itself are restarted.
9
10
 
10
- ### Requirements
11
-
12
- Deploying a Rails application with Conjure currently requires the
13
- following:
14
-
15
- * A DigitalOcean account
11
+ Conjure sets up an environment suitable for running the Rails application into which you've installed the Conjure gem, but it does not install your application. See the [capistrano-conjure](https://github.com/brianauton/capistrano-conjure) gem for an easy way to combine Conjure with deployment of your application.
16
12
 
17
- * A Public/private SSH keypair to use for bootstrapping new cloud
18
- servers. Generating a new keypair for this purpose is recommended.
13
+ WARNING: Conjure creates server instances and other resources using service provider accounts that you specify. In most cases this incurs service charges that will recur until you explicitly cancel them. You are responsible for all charges incurred through your use of Conjure.
19
14
 
20
- Also, your Rails application requires all of the following:
15
+ WARNING: Conjure attempts to have a good security model and to treat your code and data responsibly, but this is not guaranteed. You are responsible for the security and confidentiality of the code of applications deployed with Conjure, the data handled by these applications, and your service credentials and public keys used for deployment.
21
16
 
22
- * It must have a `.ruby-version` file indicating which version of
23
- Ruby to run
17
+ ### Requirements
24
18
 
25
- * It must be able to run in production mode with a single Postgres
26
- or MySQL database (any existing database.yml will be ignored)
19
+ * A DigitalOcean account (other cloud services may be supported in the future).
27
20
 
28
- * It must be checked out locally into a git repository with a valid
29
- `origin` remote
21
+ * A public SSH key located in `~/.ssh/id_rsa.pub`. This public key will be uploaded to the instance to allow subsequent access to the server. (other methods of granting server access to developers may be supported in the future).
30
22
 
31
- * The public SSH key you're using must have permission to check out
32
- the project from `origin`
23
+ * A Rails app that uses Postgres as its database (other databases may be supported in the future).
33
24
 
34
25
  ### Getting Started
35
26
 
36
- First, install the Conjure gem by either adding it to your Gemfile
27
+ First, install the Conjure gem either by adding it to your Gemfile
37
28
 
38
29
  group :development do
39
30
  gem "conjure"
@@ -43,84 +34,52 @@ and then running `bundle`, OR by installing it directly:
43
34
 
44
35
  gem install conjure
45
36
 
46
- Then add a file to your Rails project called
47
- `config/conjure.yml`. This should be a YAML file with the following
48
- fields:
49
-
50
- * `digitalocean_client_id` and `digitalocean_api_key`: These
51
- credentials are available after logging in to your Digital Ocean
52
- account.
53
-
54
- * `digitalocean_region`: The geographic region for deploying new
55
- cloud servers. If unsure, use "New York 1".
56
-
57
- * `private_key_file` and `public_key_file` (optional): Pathnames to
58
- local files (absolute paths, or relative to your project's
59
- `config` directory) that contain the private and public SSH keys
60
- to use for deployment. If these aren't specified, Conjure will try
61
- to find identity files in `~/.ssh`.
62
-
63
- Here's an example conjure.yml file:
64
-
65
- digitalocean_client_id: XXXXXXXX
66
- digitalocean_api_key: XXXXXXXX
67
- digitalocean_region: New York 1
37
+ Then set the DIGITALOCEAN_API_TOKEN environment variable to your DigitalOcean API token. Note that you need a single token that was generated for DigitalOcean's v2 API, not a key and secret pair as were used with their older v1 API.
68
38
 
69
- Finally, tell Conjure to deploy your app:
39
+ export DIGITALOCEAN_API_TOKEN=xxxxxxxxxxxxxxxx
70
40
 
71
- conjure deploy
41
+ Now you're ready to provision a new instance of the Rails app. Here's an example of Ruby code you could run from the Rails console or from a Rake task in your Rails app:
72
42
 
73
- The last line of the output will tell you the IP address of the
74
- deployed server. Repeating the command will reuse the existing server
75
- rather than deploying a new one. Specify a branch to deploy with
76
- `--branch` or `-b` (default is `master`):
43
+ Conjure::Instance.create app_name: "widget_store", rails_env: "demo", ruby_version: "2.0"
77
44
 
78
- conjure deploy --branch=mybranch
45
+ ### Creating a new instance
79
46
 
80
- ### Additional Commands
47
+ To create a new instance on a new DigitalOcean droplet, call `Conjure::Instance.create` with a list of options. The following options are required:
81
48
 
82
- Additional commands are available after you've deployed with `conjure
83
- deploy`.
49
+ * app_name: The application name; included in the name of the DigitalOcean droplet for easy identification.
84
50
 
85
- #### Export
51
+ Conjure will create the instance, and then give you a summary of information about the instance (including its IP address) that you'll need in order to access the instance and deploy your application.
86
52
 
87
- Produce a native-format (Postgres or MySQL) dump of the
88
- currently-deployed server's production database, and save it to the
89
- local file `FILE`.
53
+ ### Updating an existing instance
90
54
 
91
- conjure export FILE
55
+ To update an existing instance, call `Conjure::Instance.update` with a list of options. The following options are required:
92
56
 
93
- #### Import
57
+ * ip_address: The IP address of the DigitalOcean droplet to update.
94
58
 
95
- Overwrite the production database on the currently-deployed server
96
- with a dump from the local file `FILE`. The dump should be in the same
97
- format as that produced by the `export` command (either a Postgres or
98
- MySQL dump according to the database type).
59
+ Updating an instance simply involves checking that all the necessary Docker containers are running, and starting any that aren't running (after building them according to the supplied options). This means that if you want to change the configuration of your Rails application's web server, you can SSH to the droplet and manually stop and remove the `passenger` container, then run an update to have it rebuilt. This will preserve all your application's data, since that is stored in volume containers. An easier method for changing the options on existing instances may be added in the future.
99
60
 
100
- conjure import FILE
61
+ ### Options
101
62
 
102
- #### Log
63
+ The following additional options are supported for both `create` and `update`:
103
64
 
104
- Show logs from the deployed application. Optionally specify the number
105
- of lines with `-n`, and use --tail to continue streaming new lines as
106
- they are added to the log.
65
+ * max_upload_mb: The maximum size in megabytes of uploaded files that the web server should allow. Default is 20.
107
66
 
108
- conjure log [-n=NUM] [--tail|-t]
67
+ * rails_env: The Rails environment, e.g. "staging" or "production". Default is "staging".
109
68
 
110
- #### Console
69
+ * ruby_version: Valid values are "1.9", "2.0", "2.1", and "2.2" (the default).
111
70
 
112
- Open a console on the deployed application.
71
+ * rubygems_version: Use this only if your application requires a specific version of RubyGems, otherwise a reasonably recent version will be used.
113
72
 
114
- conjure console
73
+ * ssl_hostname: Optionally configure the web server to respond to SSL connections at the given hostname. Currently this requires you to upload certificate and key files to the server after the instance is created.
115
74
 
116
- #### Rake
75
+ * system_packages: An array of the names of any additional `apt` packages that should be installed in the Ubuntu container that runs your Rails app.
117
76
 
118
- Run a rake task on the deployed application and show the output.
77
+ ### Development
119
78
 
120
- conjure rake [ARGUMENTS...]
79
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec conjure` to use the gem in this directory, ignoring other installed copies of this gem.
121
80
 
122
- #### Show
81
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
123
82
 
124
- List the current status of all deployed instances of your command.
83
+ ### Contributing
125
84
 
126
- conjure show
85
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/brianauton/conjure](https://github.com/brianauton/conjure).
@@ -1,12 +1 @@
1
- module Conjure
2
- require File.join(File.dirname(__FILE__), "conjure/provider")
3
- Dir[File.join(File.dirname(__FILE__), "conjure/**/*.rb")].each { |f| require f }
4
-
5
- def self.config
6
- @config ||= Config.load Dir.pwd
7
- end
8
-
9
- def self.identity
10
- @identity ||= Identity.new(config)
11
- end
12
- end
1
+ require "conjure/instance"
@@ -0,0 +1,39 @@
1
+ module Conjure
2
+ class DelayedJob
3
+ def initialize(options)
4
+ @rails_env = options[:rails_env]
5
+ end
6
+
7
+ def apply(template)
8
+ template.add_file_data monit_rc, "/etc/monit/monitrc"
9
+ template.run "chmod 0600 /etc/monit/monitrc"
10
+ template.add_file_data monit_run, "/etc/service/monit/run"
11
+ template.run "chmod 0700 /etc/service/monit/run"
12
+ end
13
+
14
+ def system_packages
15
+ ["monit"]
16
+ end
17
+
18
+ private
19
+
20
+ def monit_rc
21
+ command = "/usr/bin/env RAILS_ENV=#{@rails_env} /home/app/application/current/bin/delayed_job"
22
+ 'set daemon 10
23
+ set logfile /var/log/monit.log
24
+ set idfile /var/lib/monit/id
25
+ set statefile /var/lib/monit/state
26
+ check process delayed_job
27
+ with pidfile /home/app/application/shared/tmp/pids/delayed_job.pid
28
+ start program = "' + command + ' start" as uid "app" and gid "app"
29
+ stop program = "' + command + ' stop" as uid "app" and gid "app"
30
+ '
31
+ end
32
+
33
+ def monit_run
34
+ '#!/bin/sh
35
+ exec /usr/bin/monit -I
36
+ '
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,6 @@
1
1
  require "conjure/digital_ocean/account"
2
2
  require "conjure/digital_ocean/key_set"
3
+ require "securerandom"
3
4
 
4
5
  module Conjure
5
6
  module DigitalOcean
@@ -20,9 +21,10 @@ module Conjure
20
21
  private
21
22
 
22
23
  def create
24
+ puts "Creating DigitalOcean droplet..."
23
25
  response = account.post("droplets", {
24
26
  image: @options[:image],
25
- name: @options[:name],
27
+ name: "#{@options[:name_prefix]}-#{SecureRandom.hex 4}",
26
28
  region: @options[:region],
27
29
  size: @options[:size],
28
30
  ssh_keys: [key.id],
@@ -39,7 +41,8 @@ module Conjure
39
41
  end
40
42
 
41
43
  def account
42
- @account ||= Account.new(:token => @options[:token])
44
+ raise "Error: DIGITALOCEAN_API_TOKEN must be set." unless ENV["DIGITALOCEAN_API_TOKEN"]
45
+ @account ||= Account.new(:token => ENV["DIGITALOCEAN_API_TOKEN"])
43
46
  end
44
47
 
45
48
  def key
@@ -0,0 +1,75 @@
1
+ module Conjure
2
+ module Docker
3
+ class Host
4
+ def initialize(server)
5
+ @server = server
6
+ end
7
+
8
+ def start(image_name, daemon_command, options = {})
9
+ container_name = options[:name]
10
+ all_options = "#{start_options options} #{image_name} #{daemon_command}"
11
+ if running? container_name
12
+ puts "Detected #{container_name} container running."
13
+ else
14
+ puts "Starting #{container_name} container..."
15
+ @server.run("docker run #{all_options}").strip
16
+ sleep 2
17
+ raise "Container failed to start" unless running? container_name
18
+ end
19
+ end
20
+
21
+ def build(image_source_files)
22
+ Dir.mktmpdir do |dir|
23
+ image_source_files.each { |filename, data| File.write "#{dir}/#{filename}", data }
24
+ result = with_directory(dir) { |remote_dir| @server.run "docker build #{remote_dir}" }
25
+ match = result.match(/Successfully built ([0-9a-z]+)/)
26
+ raise "Failed to build Docker image, output was #{result}" unless match
27
+ match[1]
28
+ end
29
+ end
30
+
31
+ def running?(container_name)
32
+ running_container_names.include? container_name
33
+ end
34
+
35
+ private
36
+
37
+ def with_directory(local_path, &block)
38
+ local_archive = remote_archive = "/tmp/archive.tar.gz"
39
+ remote_path = "/tmp/unpacked_archive"
40
+ `cd #{local_path}; tar czf #{local_archive} *`
41
+ @server.send_file local_archive, remote_archive
42
+ @server.run "mkdir #{remote_path}; cd #{remote_path}; tar mxzf #{remote_archive}"
43
+ yield remote_path
44
+ ensure
45
+ `rm #{local_archive}`
46
+ @server.run "rm -Rf #{remote_path} #{remote_archive}"
47
+ end
48
+
49
+ def start_options(options)
50
+ [
51
+ "-d",
52
+ "--restart=always",
53
+ mapped_options("--link", options[:linked_containers]),
54
+ ("--name #{options[:name]}" if options[:name]),
55
+ mapped_options("-p", options[:ports]),
56
+ listed_options("--volumes-from", options[:volume_containers]),
57
+ ].flatten.compact.join(" ")
58
+ end
59
+
60
+ def listed_options(command, values)
61
+ values ||= []
62
+ values.map { |v| "#{command} #{v}" }
63
+ end
64
+
65
+ def mapped_options(command, values)
66
+ values ||= {}
67
+ values.map { |from, to| "#{command} #{from}:#{to}" }
68
+ end
69
+
70
+ def running_container_names
71
+ @server.run("docker ps --format='{{.Names}}'").split("\n").compact
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,71 @@
1
+ require "conjure/docker/host"
2
+ require "tmpdir"
3
+
4
+ module Conjure
5
+ module Docker
6
+ class Template
7
+ def initialize(base_image_name)
8
+ @commands = ["FROM #{base_image_name}"]
9
+ @file_data = {}
10
+ end
11
+
12
+ def add_file(filename, remote_name)
13
+ add_file_data File.read(filename), remote_name
14
+ end
15
+
16
+ def add_file_data(data, remote_name)
17
+ local_name = "file#{@file_data.length+1}"
18
+ @file_data[local_name] = data
19
+ @commands << "ADD #{local_name} #{remote_name}"
20
+ end
21
+
22
+ def run(command)
23
+ @commands << "RUN #{command}"
24
+ end
25
+
26
+ def volume(name)
27
+ @commands << "VOLUME #{name}"
28
+ end
29
+
30
+ def source
31
+ @commands.join "\n"
32
+ end
33
+
34
+ def environment(hash)
35
+ hash.each { |key, value| @commands << "ENV #{key} #{value}" }
36
+ end
37
+
38
+ def start(container_host, command, options = {})
39
+ if container_names(options).all? { |name| container_host.running? name }
40
+ puts "Detected all #{options[:name]} containers running."
41
+ else
42
+ puts "Building #{options[:name]} base image..."
43
+ image_name = container_host.build(image_source_files)
44
+ options = options.merge(volume_options(container_host, image_name, options)) if options[:volumes]
45
+ container_host.start(image_name, command, options)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def container_names(options)
52
+ [options[:name]] + options[:volumes].to_h.keys
53
+ end
54
+
55
+ def volume_options(container_host, image_name, options)
56
+ {
57
+ volume_containers: options[:volumes].map do |name, path|
58
+ volume_template = Docker::Template.new(image_name)
59
+ volume_template.volume path
60
+ volume_template.start(container_host, "/bin/true", name: name)
61
+ name
62
+ end
63
+ }
64
+ end
65
+
66
+ def image_source_files
67
+ @file_data.merge "Dockerfile" => @commands.join("\n")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,102 +1,57 @@
1
+ require "conjure/docker/host"
2
+ require "conjure/rails_application"
3
+ require "conjure/server"
4
+ require "conjure/swap"
5
+ require "yaml"
6
+
1
7
  module Conjure
2
8
  class Instance
3
- def initialize(options)
4
- @origin = options[:origin]
5
- @branch = options[:branch]
6
- @rails_environment = options[:rails_environment]
7
- @server = options[:server]
9
+ def initialize(ip_address, options)
10
+ @server = Server.new ip_address
11
+ @options = options
8
12
  end
9
13
 
10
- def self.where(options = {})
11
- Collection.new(options)
14
+ def self.create(options)
15
+ @server = Server.create server_name_prefix(options), options
16
+ new(@server.ip_address, options).tap(&:update)
12
17
  end
13
18
 
14
- def origin
15
- @origin ||= @server.name.split("-")[0]
19
+ def self.update(options)
20
+ ip_address = options.delete(:ip_address)
21
+ new(ip_address, options).tap(&:update)
16
22
  end
17
23
 
18
- def rails_environment
19
- @rails_environment ||= @server.name.split("-")[1]
24
+ def update
25
+ components.each(&:install)
20
26
  end
21
27
 
22
28
  def ip_address
23
29
  @server.ip_address
24
30
  end
25
31
 
26
- def shell
27
- rails_server.base_image
28
- end
29
-
30
- def branch
31
- @branch ||= codebase.branch
32
+ def port
33
+ 2222
32
34
  end
33
35
 
34
- def database
35
- codebase.database
36
+ def user
37
+ "app"
36
38
  end
37
39
 
38
- def create
39
- @server_name = Service::CloudServer.ensure_unique_name(server_name)
40
- deploy
40
+ def pending_files
41
+ components.flat_map(&:pending_files)
41
42
  end
42
43
 
43
- def deploy
44
- Log.info "[deploy] Deploying #{branch} to #{rails_environment}"
45
- codebase.install
46
- rails_server.run
47
- Log.info "[deploy] Application deployed to #{ip_address}"
48
- end
49
-
50
- def codebase
51
- @codebase ||= Service::RailsCodebase.new target, origin, @branch, rails_environment
52
- end
44
+ private
53
45
 
54
- def rails_server
55
- @rails_server ||= Service::RailsServer.new target, rails_environment
46
+ def self.server_name_prefix(options)
47
+ "#{options[:app_name]}-#{options[:rails_env]}"
56
48
  end
57
49
 
58
- def server_name
59
- @server_name ||= "#{application_name}-#{rails_environment}"
60
- end
61
-
62
- def server
63
- @server ||= Service::CloudServer.new(server_name)
64
- end
65
-
66
- def target
67
- @target ||= Target.new(:machine_name => server.name)
68
- end
69
-
70
- def application_name
71
- Application.new(:origin => @origin).name
72
- end
73
-
74
- def status
75
- "running"
76
- end
77
-
78
- def name
79
- server.name
80
- end
81
-
82
- class Collection
83
- include Enumerable
84
-
85
- def initialize(options)
86
- @origin = options[:origin]
87
- end
88
-
89
- def application_name
90
- Application.new(:origin => @origin).name
91
- end
92
-
93
- def each(&block)
94
- return unless @origin
95
- Service::CloudServer.each_with_name_prefix("#{application_name}-") do |server|
96
- match = server.name.match(/^#{application_name}-([^-]+)(-[0-9]+)?$/)
97
- yield Instance.new(:server => server) if match
98
- end
99
- end
50
+ def components
51
+ @components ||= [
52
+ Swap.new(@server),
53
+ RailsApplication.new(Docker::Host.new(@server), @options),
54
+ ]
100
55
  end
101
56
  end
102
57
  end