ruby_yacht 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.
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