docker-rails 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/docker-rails.gemspec CHANGED
@@ -20,9 +20,11 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.add_development_dependency 'bundler', '~> 1.6'
22
22
  s.add_development_dependency 'rake'
23
+ s.add_development_dependency 'rspec'
24
+ s.add_development_dependency 'mysql2', '~> 0.3.18' # http://stackoverflow.com/a/32466950/2363935
25
+ s.add_development_dependency 'activerecord'
23
26
 
24
27
  s.add_dependency 'docker-api'
25
- # s.add_dependency 'parallel_tests'
26
- s.add_dependency 'dry-config', '>= 1.1.6'
27
- s.add_dependency 'mysql2', '~> 0.3.18' # http://stackoverflow.com/a/32466950/2363935
28
+ s.add_dependency 'dry-config', '>= 1.2.6'
29
+ s.add_dependency 'thor'
28
30
  end
data/lib/docker/rails.rb CHANGED
@@ -5,4 +5,14 @@ module Docker
5
5
  end
6
6
  end
7
7
 
8
- require 'docker/rails/compose_config'
8
+ require 'thor'
9
+ require 'docker'
10
+
11
+ require 'docker/rails/config'
12
+ require 'docker/rails/compose_config'
13
+ require 'docker/rails/app'
14
+
15
+ require 'docker/rails/cli/db_check'
16
+ require 'docker/rails/cli/gems_volume'
17
+
18
+ require 'docker/rails/cli/main'
@@ -0,0 +1,67 @@
1
+ module Docker
2
+ module Rails
3
+ module CLI
4
+ require 'thor'
5
+ class DbCheck < Thor
6
+
7
+ default_task :help
8
+
9
+ desc 'mysql', 'Ping and wait for mysql database to be up'
10
+ option :count, default: 60, desc: 'Number of attempts'
11
+ option :host, default: 'db'
12
+ option :port, default: 3306
13
+ option :username, default: 'root'
14
+ option :password, desc: 'Password-less login if unspecified'
15
+
16
+ def mysql
17
+
18
+ App.instance
19
+
20
+ # ping db to see if it is ready before continuing
21
+ require 'rubygems'
22
+ require 'active_record'
23
+ require 'mysql2'
24
+
25
+ puts "\n"
26
+ connect_string = "#{options[:username]}@#{options[:host]}:#{options[:port]}"
27
+ printf "Waiting for confirmation of db service startup at #{connect_string}..."
28
+ last_message = ''
29
+ loop_limit = options[:count].to_i + 1
30
+ loop_limit.times do |i|
31
+ if i == loop_limit - 1
32
+ printf "failed to connect. #{last_message}\n\n\n"
33
+ raise "Failed to connect to db service at #{connect_string}. #{last_message}"
34
+ end
35
+
36
+ connection_options = {
37
+ adapter: 'mysql2',
38
+ host: options[:host],
39
+ port: options[:port],
40
+ username: options[:username]
41
+ }
42
+
43
+ #puts "Password is nil? #{options[:password].nil?}, |#{options[:password]}|"
44
+
45
+ connection_options[:password] = options[:password] unless options[:password].nil?
46
+
47
+ ActiveRecord::Base.establish_connection (connection_options)
48
+ connected =
49
+ begin
50
+ ActiveRecord::Base.connection_pool.with_connection { |con| con.active? }
51
+ rescue => e
52
+ last_message = "#{e.class.name}: #{e.message}"
53
+ false
54
+ end
55
+ printf '.'
56
+ if connected
57
+ printf 'connected.'
58
+ break
59
+ end
60
+ sleep 1
61
+ end
62
+ puts "\n"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ module Docker
2
+ module Rails
3
+ module CLI
4
+ class GemsVolume < Thor
5
+
6
+ default_task :help
7
+
8
+ desc 'create', 'Create a gem volume'
9
+ def create(build_name = nil, environment_name = nil)
10
+ # Create global gems data volume to cache gems for this version of ruby
11
+ app = App.configured(build_name, environment_name)
12
+ begin
13
+ Docker::Container.get(app.gems_volume_name)
14
+ puts "Gem data volume container #{app.gems_volume_name} already exists."
15
+ rescue Docker::Error::NotFoundError => e
16
+
17
+ exec "docker create -v #{app.gems_volume_path} --name #{app.gems_volume_path} busybox"
18
+ puts "Gem data volume container #{app.gems_volume_name} created."
19
+ end
20
+ end
21
+
22
+
23
+ # TODO: add destroy volume
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,117 @@
1
+ module Docker
2
+ module Rails
3
+ module CLI
4
+ class Main < Thor
5
+ # default_task :help
6
+
7
+ desc 'db_check <db>', 'Runs db_check'
8
+ subcommand 'db_check', Docker::Rails::CLI::DbCheck
9
+
10
+
11
+ desc 'gems_volume <command>', 'Gems volume management'
12
+ subcommand 'gems_volume', Docker::Rails::CLI::GemsVolume
13
+
14
+
15
+ desc 'ci <build_name> <environment_name>', 'Execute the works, everything with cleanup included i.e. bundle exec docker-rails ci 222 test'
16
+ long_desc <<-D
17
+
18
+ `ci` will run the targeted environment_name with the given build number then cleanup everything upon completion.
19
+ While it is named `ci`, there is no harm in using this for other environments as long as you understand that volumes
20
+ and remaining dangling images will be cleaned up upon completion.
21
+ D
22
+
23
+ def ci(build_name, environment_name)
24
+ invoke :compose
25
+ invoke CLI::GemsVolume, :create
26
+ invoke :before
27
+ begin
28
+ invoke :up
29
+ ensure
30
+ invoke :stop
31
+ invoke :rm_volumes
32
+ invoke :rm_compose
33
+ invoke :rm_dangling
34
+ invoke :show_all_containers
35
+ end
36
+ end
37
+
38
+ desc 'compose <build_name> <environment_name>', 'Writes a resolved docker-compose.yml file'
39
+
40
+ def compose(build_name, environment_name)
41
+ App.configured(build_name, environment_name).compose
42
+ end
43
+
44
+ desc 'before <build_name> <environment_name>', 'Invoke before_command', hide: true
45
+
46
+ def before(build_name, environment_name)
47
+ invoke :compose
48
+ App.configured(build_name, environment_name).exec_before_command
49
+ end
50
+
51
+ desc 'up <build_name> <environment_name>', 'Up the docker-compose configuration for the given build_name/environment_name'
52
+
53
+ def up(build_name, environment_name)
54
+
55
+ invoke CLI::GemsVolume, :create
56
+ invoke :before
57
+ App.configured(build_name, environment_name).exec_up
58
+ end
59
+
60
+ desc 'stop <build_name> <environment_name>', 'Stop all running containers for the given build_name/environment_name'
61
+
62
+ def stop(build_name, environment_name)
63
+ invoke :compose
64
+ App.configured(build_name, environment_name).exec_stop
65
+ end
66
+
67
+ desc 'rm_volumes <build_name> <environment_name>', 'Stop all running containers and remove corresponding volumes for the given build_name/environment_name'
68
+
69
+ def rm_volumes(build_name, environment_name)
70
+ invoke :stop
71
+ App.configured(build_name, environment_name).exec_remove_volumes
72
+ end
73
+
74
+ desc 'rm_compose', 'Remove generated docker_compose file'
75
+
76
+ def rm_compose(build_name = nil, environment_name = nil)
77
+ App.instance.rm_compose
78
+ end
79
+
80
+ desc 'rm_dangling', 'Remove danging images'
81
+
82
+ def rm_dangling(build_name = nil, environment_name = nil)
83
+ App.instance.rm_dangling
84
+ end
85
+
86
+ desc 'show_all_containers', 'Show all remaining containers regardless of state'
87
+
88
+ def show_all_containers(build_name = nil, environment_name = nil)
89
+ App.instance.show_all_containers
90
+ end
91
+
92
+
93
+ # desc 'hello NAME', 'This will greet you'
94
+ # long_desc <<-HELLO_WORLD
95
+ #
96
+ # `hello NAME` will print out a message to the person of your choosing.
97
+ #
98
+ # Brian Kernighan actually wrote the first "Hello, World!" program
99
+ # as part of the documentation for the BCPL programming language
100
+ # developed by Martin Richards. BCPL was used while C was being
101
+ # developed at Bell Labs a few years before the publication of
102
+ # Kernighan and Ritchie's C book in 1972.
103
+ #
104
+ # http://stackoverflow.com/a/12785204
105
+ # HELLO_WORLD
106
+ #
107
+ # option :upcase
108
+ #
109
+ # def hello(name)
110
+ # greeting = "Hello, #{name}"
111
+ # greeting.upcase! if options[:upcase]
112
+ # puts greeting
113
+ # end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,176 @@
1
+ module Docker
2
+ module Rails
3
+ require 'singleton'
4
+ class App
5
+ include Singleton
6
+ attr_reader :config, :compose_config, :ruby_version, :build_name, :environment_name, :gems_volume_path, :gems_volume_name, :compose_filename
7
+
8
+ class << self
9
+ def configured(build_name, environment_name)
10
+ app = App.instance
11
+ if app.is_configured?
12
+ puts "Already configured"
13
+ else
14
+ app.configure(build_name: build_name, environment_name: environment_name)
15
+ end
16
+ app
17
+ end
18
+ end
19
+
20
+ def initialize
21
+ discover_ruby_version
22
+ set_gems_volume_vars
23
+ end
24
+
25
+ def configure(options)
26
+ ENV['BUILD_NAME'] = @build_name = options[:build_name]
27
+ @environment_name = options[:environment_name]
28
+
29
+ # load the docker-rails.yml
30
+ @config = Docker::Rails::Config.new
31
+ @config.load!(@environment_name)
32
+
33
+ @is_configured = true
34
+ end
35
+
36
+ def is_configured?
37
+ @is_configured || false
38
+ end
39
+
40
+ def compose
41
+ # Write a docker-compose.yml with interpolated variables
42
+ @compose_filename = compose_filename_from @build_name, @environment_name
43
+
44
+ rm_compose
45
+
46
+ @config.write_docker_compose_file(@compose_filename)
47
+
48
+ @compose_config = Docker::Rails::ComposeConfig.new
49
+ @compose_config.load!(nil, @compose_filename)
50
+ end
51
+
52
+ def rm_compose
53
+ # Delete old docker compose files
54
+ exec "rm #{compose_filename_from '*', '*'}" rescue ''
55
+ end
56
+
57
+ def exec_before_command
58
+ before_command = @config['before_command']
59
+ (exec before_command unless before_command.nil?) #unless skip? :before_command
60
+ end
61
+
62
+ def exec_up
63
+ # Run the compose configuration
64
+ exec_compose 'up' #unless skip? :up
65
+ end
66
+
67
+ def exec_stop
68
+ puts "\n\n\n\nStopping containers..."
69
+ puts '-----------------------------'
70
+ @compose_config.each_key do |service_name|
71
+ stop(service_name)
72
+ end
73
+ # puts "\nDone."
74
+ end
75
+
76
+ def exec_remove_volumes
77
+ puts "\n\nRemoving container volumes..."
78
+ puts '-----------------------------'
79
+ @compose_config.each_key do |service_name|
80
+ rm_v(service_name)
81
+ end
82
+ # puts "\nDone."
83
+ end
84
+
85
+ def rm_dangling
86
+ puts "\n\nCleaning up dangling images..."
87
+ puts '-----------------------------'
88
+ exec 'docker images --filter dangling=true -q | xargs docker rmi'
89
+ # puts "\nDone."
90
+ end
91
+
92
+ def show_all_containers
93
+ puts "\n\nAll remaining containers..."
94
+ puts '-----------------------------'
95
+ system 'docker ps -a'
96
+ end
97
+
98
+ protected
99
+
100
+ def exec(cmd, capture = false)
101
+ puts "Running `#{cmd}`" if verbose?
102
+ if capture
103
+ output = %x[#{cmd}]
104
+ else
105
+ system cmd
106
+ end
107
+
108
+ raise "Failed to execute: `#{cmd}`" unless $?.success?
109
+ output
110
+ end
111
+
112
+ # convenience to execute docker-compose with file and project params
113
+ def exec_compose(cmd, capture = false)
114
+ exec("docker-compose -f #{@compose_filename} -p #{App.instance.build_name} #{cmd}", capture)
115
+ end
116
+
117
+ # service_name i.e. 'db' or 'web'
118
+ def get_container_name(service_name)
119
+ output = exec_compose "ps #{service_name}", true
120
+ # puts "get_container(#{service_name}): \n#{output}"
121
+ output =~ /^(\w+)/ # grab the name, only thing that is at the start of the line
122
+ $1
123
+ end
124
+
125
+ # def up_service(service_name, options = '')
126
+ # exec_compose "up #{options} #{service_name}"
127
+ # container_name = get_container_name(service_name)
128
+ # puts "#{service_name}: container_name #{container_name}"
129
+ #
130
+ # container = Docker::Container.get(container_name)
131
+ # # container.streaming_logs(stdout: true) { |stream, chunk| puts "#{service_name}: #{chunk}" }
132
+ # # puts container
133
+ #
134
+ # {service_name => {'container' => container, 'container_name' => container_name}}
135
+ # end
136
+
137
+ def rm_v(service_name)
138
+ exec_compose "rm -v --force #{service_name}"
139
+ end
140
+
141
+ def stop(service_name)
142
+ exec_compose "stop #{service_name}"
143
+ end
144
+
145
+ # def skip?(command)
146
+ # skips = @config[:skip]
147
+ # return false if skips.nil?
148
+ # skip = skips.include? command.to_s
149
+ # puts "Skipping #{command}" if skip && verbose?
150
+ # skip
151
+ # end
152
+
153
+
154
+ def verbose?
155
+ @verbose ||= (@config['verbose'] unless @config.nil?) || false
156
+ end
157
+
158
+ def set_gems_volume_vars
159
+ # Set as variable for interpolation
160
+ ENV['GEMS_VOLUME_PATH'] = @gems_volume_path = "/gems/#{@ruby_version}"
161
+ ENV['GEMS_VOLUME_NAME'] = @gems_volume_name = "gems-#{@ruby_version}"
162
+ end
163
+
164
+ def discover_ruby_version
165
+ # Discover ruby version from the Dockerfile image
166
+ IO.read('Dockerfile') =~ /^FROM \w+\/ruby:(\d+.\d+(?:.\d+))/
167
+ @ruby_version = $1
168
+ end
169
+
170
+ # accessible so that we can delete patterns
171
+ def compose_filename_from(build_name, environment_name)
172
+ "docker-compose-build-#{build_name}-#{environment_name}.yml"
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,40 @@
1
+ module Docker
2
+ module Rails
3
+ require 'dry/config'
4
+ class Config < Dry::Config::Base
5
+ def initialize(options = {})
6
+ super({
7
+ default_configuration: {
8
+ verbose: false
9
+
10
+ },
11
+ prune: [:development, :test, :parallel_tests, :staging, :production]
12
+ }.merge(options))
13
+ end
14
+
15
+ def load!(environment, *filenames)
16
+ if environment.nil?
17
+ puts 'Environment unspecified, generating docker-compose.yml based on root docker-compose yaml key.'
18
+ environment = 'docker-compose'
19
+ end
20
+
21
+ if filenames.empty?
22
+ puts 'Using docker-rails.yml'
23
+ filenames = ['docker-rails.yml']
24
+ end
25
+
26
+ super(environment, *filenames)
27
+ end
28
+
29
+ def write_docker_compose_file(output_filename = 'docker-compose.yml')
30
+ write_yaml_file(output_filename, self[:'docker-compose'])
31
+ end
32
+
33
+ def to_yaml(config = @configuration)
34
+ yaml = super(config)
35
+ yaml = yaml.gsub(/command: .$/, 'command: >')
36
+ yaml
37
+ end
38
+ end
39
+ end
40
+ end