ruby_yacht 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +27 -0
  3. data/.gitignore +4 -0
  4. data/.rdoc_options +29 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +1156 -0
  7. data/.travis.yml +4 -0
  8. data/Gemfile +3 -0
  9. data/Gemfile.lock +44 -0
  10. data/LICENSE +8 -0
  11. data/README.md +216 -0
  12. data/doc/CONTRIBUTING.md +12 -0
  13. data/doc/TODO.md +28 -0
  14. data/doc/configuration_sample.rb +87 -0
  15. data/lib/ruby_yacht/dsl/app.rb +59 -0
  16. data/lib/ruby_yacht/dsl/configuration.rb +55 -0
  17. data/lib/ruby_yacht/dsl/database.rb +57 -0
  18. data/lib/ruby_yacht/dsl/dns_server.rb +38 -0
  19. data/lib/ruby_yacht/dsl/dsl.rb +252 -0
  20. data/lib/ruby_yacht/dsl/project.rb +140 -0
  21. data/lib/ruby_yacht/dsl.rb +6 -0
  22. data/lib/ruby_yacht/images/app/Dockerfile.erb +32 -0
  23. data/lib/ruby_yacht/images/app/checkout.rb +7 -0
  24. data/lib/ruby_yacht/images/app/startup.rb +17 -0
  25. data/lib/ruby_yacht/images/app/update_database_config.rb +45 -0
  26. data/lib/ruby_yacht/images/app-dependencies/Dockerfile.erb +23 -0
  27. data/lib/ruby_yacht/images/app-dependencies/install_gems.rb +12 -0
  28. data/lib/ruby_yacht/images/database/Dockerfile.erb +26 -0
  29. data/lib/ruby_yacht/images/database/load_seeds.rb +42 -0
  30. data/lib/ruby_yacht/images/database/setup.rb +19 -0
  31. data/lib/ruby_yacht/images/database/setup_database.sql.erb +12 -0
  32. data/lib/ruby_yacht/images/deploy/Dockerfile.erb +2 -0
  33. data/lib/ruby_yacht/images/web/Dockerfile.erb +25 -0
  34. data/lib/ruby_yacht/images/web/add_app.rb +12 -0
  35. data/lib/ruby_yacht/images/web/add_project.rb +14 -0
  36. data/lib/ruby_yacht/images/web/app_config.erb +11 -0
  37. data/lib/ruby_yacht/images/web/index.html.erb +10 -0
  38. data/lib/ruby_yacht/images/web/index_config.erb +12 -0
  39. data/lib/ruby_yacht/images/web/setup.rb +22 -0
  40. data/lib/ruby_yacht/runner/build.rb +21 -0
  41. data/lib/ruby_yacht/runner/build_images.rb +82 -0
  42. data/lib/ruby_yacht/runner/checkout.rb +68 -0
  43. data/lib/ruby_yacht/runner/command.rb +161 -0
  44. data/lib/ruby_yacht/runner/help.rb +55 -0
  45. data/lib/ruby_yacht/runner/implode.rb +33 -0
  46. data/lib/ruby_yacht/runner/run_containers.rb +105 -0
  47. data/lib/ruby_yacht/runner/runner.rb +42 -0
  48. data/lib/ruby_yacht/runner/services.rb +79 -0
  49. data/lib/ruby_yacht/runner/shell.rb +66 -0
  50. data/lib/ruby_yacht/runner/update_hosts.rb +72 -0
  51. data/lib/ruby_yacht/runner.rb +18 -0
  52. data/lib/ruby_yacht.rb +6 -0
  53. data/ruby_yacht.gemspec +18 -0
  54. data/spec/docker/Dockerfile +5 -0
  55. data/spec/docker/build.bash +10 -0
  56. data/spec/dsl/app_spec.rb +47 -0
  57. data/spec/dsl/configuration_spec.rb +64 -0
  58. data/spec/dsl/database_spec.rb +75 -0
  59. data/spec/dsl/dns_server_spec.rb +25 -0
  60. data/spec/dsl/dsl_spec.rb +298 -0
  61. data/spec/dsl/project_spec.rb +266 -0
  62. data/spec/fixtures/app-dependencies-dockerfile +25 -0
  63. data/spec/fixtures/database-dockerfile +31 -0
  64. data/spec/fixtures/deploy-dockerfile +2 -0
  65. data/spec/fixtures/mars-dockerfile +32 -0
  66. data/spec/fixtures/multi-project-web-dockerfile +35 -0
  67. data/spec/fixtures/web-dockerfile +27 -0
  68. data/spec/runner/build_images_spec.rb +164 -0
  69. data/spec/runner/build_spec.rb +86 -0
  70. data/spec/runner/checkout_spec.rb +128 -0
  71. data/spec/runner/command_spec.rb +94 -0
  72. data/spec/runner/help_spec.rb +73 -0
  73. data/spec/runner/implode_spec.rb +62 -0
  74. data/spec/runner/run_containers_spec.rb +141 -0
  75. data/spec/runner/runner_spec.rb +117 -0
  76. data/spec/runner/services_spec.rb +135 -0
  77. data/spec/runner/shell_spec.rb +123 -0
  78. data/spec/runner/update_hosts_spec.rb +163 -0
  79. data/spec/spec_helper.rb +10 -0
  80. data/spec/support/docker_stubbing.rb +93 -0
  81. data/spec/support/test_project.rb +56 -0
  82. metadata +193 -0
@@ -0,0 +1,79 @@
1
+ module RubyYacht::Runner
2
+ # This class provides a command for starting, stopping, and restarting
3
+ # containers.
4
+ class Services < Command
5
+ # The name of the command.
6
+ def self.command; 'services'; end
7
+
8
+ # The description for the command.
9
+ def self.description; 'Start, stop, or restart containers'; end
10
+
11
+ # This method gets the command-line flags for this command.
12
+ def option_parser
13
+ OptionParser.new do |options|
14
+ usage = "Usage: #{Command.short_script_name} #{self.class.command} [COMMAND]"
15
+ usage += "\n\n#{self.class.description}"
16
+ usage += "\n\n[COMMAND] is required, and can be start, stop, or restart"
17
+ options.banner = usage
18
+ end
19
+ end
20
+
21
+ # The name of the docker command that we are running.
22
+ #
23
+ # This can be `start`, `stop`, or `restart`.
24
+ attr_accessor :command
25
+
26
+ # This method extracts the arguments from the command line.
27
+ #
28
+ # ### Parameters
29
+ #
30
+ # * **arguments: Array** The command-line arguments.
31
+ def parse_positional_arguments(arguments)
32
+ self.command = arguments.shift
33
+ end
34
+
35
+ # This method runs the logic for the command.
36
+ def run
37
+ if self.command == nil
38
+ log "You must provide a command"
39
+ log "Run #{Command.short_script_name} help services for more information"
40
+ return false
41
+ end
42
+
43
+ unless %w(start stop restart).include?(command)
44
+ log "#{command} is not a valid docker command"
45
+ log "Run #{Command.short_script_name} help services for more information"
46
+ return false
47
+ end
48
+
49
+ if command == 'start' && backtick('which docker-machine') != ''
50
+ start_docker_machine
51
+ end
52
+
53
+ projects.each do |project|
54
+ if project.database.local?
55
+ docker "#{command} #{project.system_prefix}-database"
56
+ end
57
+
58
+ project.apps.each do |app|
59
+ docker "#{command} #{app.container_name(project)}"
60
+ end
61
+ end
62
+ docker "#{command} #{projects.first.system_prefix}-web"
63
+
64
+ true
65
+ end
66
+
67
+ # This method starts the default docker machine.
68
+ def start_docker_machine
69
+ system "docker-machine start default"
70
+
71
+ environment_args = backtick('docker-machine env default').split("\n")
72
+ environment_args.each do |arg|
73
+ if arg =~ /export (\w*)=\"(.*)\"/
74
+ ENV[$1] = $2
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,66 @@
1
+ module RubyYacht::Runner
2
+ # This class provides a command for opening a shell in an app container.
3
+ class Shell < Command
4
+ # The name of the command.
5
+ def self.command; 'shell'; end
6
+
7
+ # The description of the command.
8
+ def self.description
9
+ "Open a shell in a container"
10
+ end
11
+
12
+ # The name of the project that the app is in.
13
+ attr_accessor :project_name
14
+
15
+ # The name of the app that we are opening a shell in.
16
+ attr_accessor :app
17
+
18
+ # The name of the command that we are invoking in the shell.
19
+ attr_accessor :command
20
+
21
+ # This method gets the command-line options for the command.
22
+ def option_parser
23
+ OptionParser.new do |options|
24
+ options.banner = "Usage: #{Command.short_script_name} shell [options] [APP] [command]"
25
+ options.separator ""
26
+ options.separator "The command is optional. If you omit it, this will open a bash shell"
27
+ options.separator ""
28
+ options.separator "Options:"
29
+
30
+ options.on('-p', '--project PROJECT', "The project with the app we are opening the sell in. Default: #{default_project.name}") do |name|
31
+ self.project_name = name.to_sym
32
+ end
33
+ end
34
+ end
35
+
36
+ # This method extracts the arguments from the command line.
37
+ #
38
+ # ### Parameters
39
+ #
40
+ # * **arguments: Array** The command-line arguments.
41
+ def parse_positional_arguments(arguments)
42
+ self.app = arguments.shift
43
+ self.command = arguments.join(' ')
44
+ if self.command == ''
45
+ self.command = 'bash'
46
+ end
47
+ end
48
+
49
+ # This method runs the logic for the command.
50
+ def run
51
+ if app.nil?
52
+ log "You must provide an app name"
53
+ log "Run #{Command.short_script_name} help shell for more information"
54
+ return false
55
+ end
56
+
57
+ project = self.project_named(project_name)
58
+ return false unless project
59
+
60
+ container = "#{project.system_prefix}-#{app}"
61
+ exec("docker exec -it #{container} bash -c 'cd /var/code; TERM=xterm #{command}'")
62
+ true
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,72 @@
1
+ module RubyYacht::Runner
2
+ # This class provides a command for updating the hosts file.
3
+ class UpdateHosts < Command
4
+ # The name of the command.
5
+ def self.command; 'update_hosts'; end
6
+
7
+ # The description of the command.
8
+ def self.description
9
+ "Add entries for your app domains to your hosts file"
10
+ end
11
+
12
+ # The current contents of the hosts file.
13
+ def current_host_contents
14
+ File.read('/etc/hosts')
15
+ end
16
+
17
+ # This method runs the logic of the command.
18
+ def run
19
+ ip_address = get_machine_info('.Driver.IPAddress')
20
+ if ip_address == ""
21
+ ip_address = "127.0.0.1"
22
+ end
23
+
24
+ current_hosts = current_host_contents.split("\n")
25
+ new_hosts = current_hosts.select do |entry|
26
+ projects.none? do |project|
27
+ entry.include?(project.domain) || entry.include?("#{project.system_prefix} docker containers")
28
+ end
29
+ end
30
+
31
+ new_hosts.pop while new_hosts.any? && new_hosts.last.length == 0
32
+
33
+ new_hosts += projects.map { |p| self.hosts_file_entries(p, ip_address) }.flatten
34
+
35
+ FileUtils.mkdir_p('tmp')
36
+ File.open(File.join('tmp', 'hosts'), 'w') do |file|
37
+ file.write(new_hosts.join("\n"))
38
+ end
39
+
40
+ timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
41
+ log "Please enter your password so that we can update the hosts file"
42
+ system "sudo cp /etc/hosts /etc/hosts.#{timestamp}.backup"
43
+ system "sudo mv tmp/hosts /etc/hosts"
44
+
45
+ true
46
+ end
47
+
48
+ # This method gets the contents of the hosts file for a project.
49
+ #
50
+ # ### Parameters
51
+ #
52
+ # * **project: RubyYacht::Project** The project we are working on.
53
+ # * **ip_address: String** The IP address that docker listens
54
+ # on.
55
+ #
56
+ # ### Returns
57
+ # An Array with the new lines for the hosts file.
58
+ def hosts_file_entries(project, ip_address)
59
+ system_prefix = project.system_prefix
60
+ main_domain = project.domain
61
+
62
+ domains = [main_domain]
63
+ domains += project.apps.map { |app| "#{app.name}.#{main_domain}" }
64
+
65
+ header = "# #{system_prefix} docker containers"
66
+ new_hosts = ["", header]
67
+ new_hosts += domains.map { |domain| "#{ip_address} #{domain}" }
68
+ new_hosts
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,18 @@
1
+ require 'fileutils'
2
+
3
+ # This module groups together classes for running commands for managing the
4
+ # docker environment.
5
+ module RubyYacht::Runner
6
+ end
7
+
8
+ require 'ruby_yacht/runner/command'
9
+ require 'ruby_yacht/runner/help'
10
+ require 'ruby_yacht/runner/build_images'
11
+ require 'ruby_yacht/runner/run_containers'
12
+ require 'ruby_yacht/runner/build'
13
+ require 'ruby_yacht/runner/services'
14
+ require 'ruby_yacht/runner/checkout'
15
+ require 'ruby_yacht/runner/implode'
16
+ require 'ruby_yacht/runner/shell'
17
+ require 'ruby_yacht/runner/update_hosts'
18
+ require 'ruby_yacht/runner/runner'
data/lib/ruby_yacht.rb ADDED
@@ -0,0 +1,6 @@
1
+ # This module groups together all the libraries for this gem.
2
+ module RubyYacht
3
+ end
4
+
5
+ require 'ruby_yacht/dsl'
6
+ require 'ruby_yacht/runner'
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'ruby_yacht'
3
+ spec.version = '0.1.0'
4
+ spec.date = '2016-04-10'
5
+ spec.summary = "A DSL for building docker containers for a family of Rails apps"
6
+ spec.authors = ["John Brownlee"]
7
+ spec.email = 'apps@mail.johnbrownlee.com'
8
+ spec.homepage = 'https://github.com/brownleej/ruby-yacht'
9
+ spec.license = 'MIT'
10
+
11
+ spec.files = `git ls-files`.split($/)
12
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
13
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
14
+
15
+ spec.add_development_dependency "rspec"
16
+ spec.add_development_dependency "codeclimate-test-reporter"
17
+ spec.add_development_dependency "timecop"
18
+ end
@@ -0,0 +1,5 @@
1
+ FROM ruby:2.3
2
+
3
+ VOLUME /var/code
4
+ WORKDIR /var/code
5
+ CMD bash
@@ -0,0 +1,10 @@
1
+ #! /bin/bash
2
+
3
+ # This script builds a docker container for running tests.
4
+ #
5
+ # This keeps the development dependencies isolated from other parts of the
6
+ # system.
7
+
8
+ docker build -t ruby-yacht-tests spec/docker
9
+ docker rm -f ruby-yacht-tests 2> /dev/null
10
+ docker run -dit --name=ruby-yacht-tests -v $PWD:/var/code ruby-yacht-tests
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubyYacht::App do
4
+ describe "DSL" do
5
+ let(:name) { :test_app }
6
+ let(:app) { RubyYacht::App::DSL.new(name).run(@builder).create_object }
7
+
8
+ it "creates an app with all fields" do
9
+ @builder = Proc.new do
10
+ repository_name 'brownleej/test-app'
11
+ port 3000
12
+ end
13
+ expect(app.name).to eq :test_app
14
+ expect(app.repository_name).to eq 'brownleej/test-app'
15
+ expect(app.port).to eq 3000
16
+ end
17
+
18
+ it "requires the repository name" do
19
+ @builder = Proc.new do
20
+ port 3000
21
+ end
22
+ expect do
23
+ self.app
24
+ end.to raise_exception("Missing required attribute repository_name for RubyYacht::App::DSL")
25
+ end
26
+
27
+ it "defaults the port to 8080" do
28
+ @builder = Proc.new do
29
+ repository_name 'brownleej/test-app'
30
+ end
31
+ expect(app.port).to eq 8080
32
+ end
33
+ end
34
+
35
+ describe "container_name" do
36
+ it "combines the project's prefix with the app's name" do
37
+ project = RubyYacht::Project.new
38
+ project.name = :test_project
39
+ project.system_prefix = :tests
40
+
41
+ app = RubyYacht::App.new
42
+ app.name = 'app1'
43
+
44
+ expect(app.container_name(project)).to eq 'tests-app1'
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'ruby_yacht'
3
+
4
+ describe RubyYacht::Configuration do
5
+ before do
6
+ RubyYacht.configuration.clear
7
+ end
8
+
9
+ describe "dsl" do
10
+ let(:project_creation_block) do
11
+ Proc.new do
12
+ project :project1 do
13
+ system_prefix :a
14
+ domain "a.test.com"
15
+ secret_key_base "a"
16
+ repository "github.com"
17
+
18
+ database do
19
+ host "localhost"
20
+ name "project1"
21
+ username "test"
22
+ password "test"
23
+ end
24
+ end
25
+
26
+ project :project2 do
27
+ system_prefix :b
28
+ domain "b.test.com"
29
+ secret_key_base "b"
30
+ repository "github.com"
31
+
32
+ database do
33
+ host "localhost"
34
+ name "project2"
35
+ username "test"
36
+ password "test"
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ it "can add projects" do
43
+ configuration = RubyYacht::Configuration::DSL.new.run(project_creation_block).create_object
44
+ expect(configuration.projects.count).to eq 2
45
+ expect(configuration.projects[0].name).to eq :project1
46
+ expect(configuration.projects[1].name).to eq :project2
47
+ end
48
+
49
+ describe "configure method" do
50
+ it "adds to the list of projects" do
51
+ project = RubyYacht::Project.new
52
+ project.name = :initial_project
53
+ RubyYacht.configuration.projects << project
54
+ RubyYacht.configure(&project_creation_block)
55
+
56
+ configuration = RubyYacht.configuration
57
+ expect(configuration.projects.count).to eq 3
58
+ expect(configuration.projects[0].name).to eq :initial_project
59
+ expect(configuration.projects[1].name).to eq :project1
60
+ expect(configuration.projects[2].name).to eq :project2
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyYacht::Database do
4
+ describe "dsl" do
5
+ let(:database) { RubyYacht::Database::DSL.new.run(@builder).create_object }
6
+
7
+ it "creates a database with all the fields" do
8
+ @builder = Proc.new do
9
+ host 'db1.test.com'
10
+ name 'apps'
11
+ username 'test-user'
12
+ password 'test-pass'
13
+ end
14
+
15
+ expect(database.host).to eq 'db1.test.com'
16
+ expect(database.name).to eq 'apps'
17
+ expect(database.username).to eq 'test-user'
18
+ expect(database.password).to eq 'test-pass'
19
+ end
20
+
21
+ it "requires the host" do
22
+ @builder = Proc.new do
23
+ name 'apps'
24
+ username 'test-user'
25
+ password 'test-pass'
26
+ end
27
+
28
+ expect { database }.to raise_exception "Missing required attribute host for RubyYacht::Database::DSL"
29
+ end
30
+
31
+ it "requires the name" do
32
+ @builder = Proc.new do
33
+ host 'db1.test.com'
34
+ username 'test-user'
35
+ password 'test-pass'
36
+ end
37
+
38
+ expect { database }.to raise_exception "Missing required attribute name for RubyYacht::Database::DSL"
39
+ end
40
+
41
+ it "requires the username" do
42
+ @builder = Proc.new do
43
+ host 'db1.test.com'
44
+ name 'apps'
45
+ password 'test-pass'
46
+ end
47
+
48
+ expect { database }.to raise_exception "Missing required attribute username for RubyYacht::Database::DSL"
49
+ end
50
+
51
+ it "requires the password" do
52
+ @builder = Proc.new do
53
+ host 'db1.test.com'
54
+ name 'apps'
55
+ username 'test-user'
56
+ end
57
+
58
+ expect { database }.to raise_exception "Missing required attribute password for RubyYacht::Database::DSL"
59
+ end
60
+ end
61
+
62
+ describe "local?" do
63
+ it "is true when the host is localhost" do
64
+ database = RubyYacht::Database.new
65
+ database.host = 'localhost'
66
+ expect(database).to be_local
67
+ end
68
+
69
+ it "is false when the host is not localhost" do
70
+ database = RubyYacht::Database.new
71
+ database.host = 'db1.test.com'
72
+ expect(database).not_to be_local
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyYacht::DnsServer do
4
+ describe "dsl" do
5
+ let(:config) { RubyYacht::DnsServer::DSL.new.run(@builder).create_object }
6
+
7
+ it "can add servers" do
8
+ @builder = Proc.new do
9
+ server '8.10.1.1'
10
+ server '8.10.1.2'
11
+ end
12
+
13
+ expect(config.servers).to eq ['8.10.1.1', '8.10.1.2']
14
+ end
15
+
16
+ it "can add search domains" do
17
+ @builder = Proc.new do
18
+ search_domain 'apps.admin.test.com'
19
+ search_domain 'web.admin.test.com'
20
+ end
21
+
22
+ expect(config.search_domains).to eq ['apps.admin.test.com', 'web.admin.test.com']
23
+ end
24
+ end
25
+ end