chbuild 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'chbuild'
6
+
7
+ require 'pry'
8
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require_relative 'lib/chbuild/version.rb'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'chbuild'
9
+ spec.version = CHBuild::VERSION
10
+ spec.author = 'Cheppers Ltd.'
11
+ spec.email = 'info@cheppers.com'
12
+ spec.summary = 'Cheppers Build Tool'
13
+ spec.homepage = 'https://github.com/Cheppers/chbuild'
14
+ spec.license = 'MIT'
15
+ spec.description = <<-EOS
16
+ chbuild is an open-source command line application written in Ruby.
17
+ It's main purpose is to simplify web app development by running a site
18
+ using Docker containers so you don't have to install Apache, PHP and
19
+ PHP extensions on your own machine.
20
+ EOS
21
+
22
+ spec.required_ruby_version = '>= 2.3.0'
23
+
24
+ if spec.respond_to?(:metadata)
25
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
26
+ else
27
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
28
+ end
29
+
30
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
31
+ spec.bindir = 'bin'
32
+ spec.executables = ['chbuild']
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.add_dependency 'colorize', '~> 0.8'
36
+ spec.add_dependency 'excon', '~> 0.46', '>= 0.46'
37
+ spec.add_dependency 'docker-api', '~> 1.31', '>= 1.31'
38
+ spec.add_dependency 'thor', '~> 0.19'
39
+
40
+ spec.add_development_dependency 'bundler', '~> 1.12'
41
+ spec.add_development_dependency 'pry', '~> 0.10'
42
+ spec.add_development_dependency 'rake', '~> 10.0'
43
+ spec.add_development_dependency 'rubocop', '~> 0.40'
44
+ spec.add_development_dependency 'rspec', '~> 3.0'
45
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'chbuild/version'
3
+
4
+ # require application files
5
+ require_relative 'chbuild/bindable_hash'
6
+ require_relative 'chbuild/constants'
7
+ require_relative 'chbuild/controller'
8
+ require_relative 'chbuild/config'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # CHBuild module :D
3
+ module CHBuild
4
+ # BindableHash is for ERB templates
5
+ class BindableHash
6
+ def initialize(hash)
7
+ hash.each do |key, value|
8
+ singleton_class.send(:define_method, key) { value }
9
+ end
10
+ end
11
+
12
+ def get_binding # rubocop:disable Style/AccessorMethodName
13
+ binding
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ require 'chbuild/config/before'
3
+ require 'chbuild/config/env'
4
+ require 'chbuild/config/errors'
5
+ require 'chbuild/config/use'
6
+ require 'chbuild/config/version'
7
+
8
+ require 'yaml'
9
+
10
+ module CHBuild
11
+ # Build configuration object
12
+ class Config
13
+ attr_reader :path, :raw, :version, :use, :env, :before
14
+
15
+ def initialize(path_to_config)
16
+ begin
17
+ @path = path_to_config
18
+ build_config = YAML.load_file(path_to_config)
19
+ rescue Errno::ENOENT
20
+ raise CHBuild::Config::NotFoundError, "'#{path_to_config}': file not found"
21
+ end
22
+
23
+ @errors = []
24
+ unless build_config
25
+ @errors << 'Build file is empty'
26
+ return
27
+ end
28
+
29
+ @raw = build_config
30
+ @version = Version.new(build_config['version'])
31
+ @use = Use.new(build_config['use'])
32
+ @env = Env.new(build_config['env'])
33
+ @before = Before.new(build_config['before'])
34
+ end
35
+
36
+ def init_script
37
+ unless @init_script
38
+ generation_time = "echo \"Generated at: [#{Time.now}]\"\n\n"
39
+ env_script = @env.to_bash_script
40
+ before_script = @before.to_bash_script
41
+ @init_script = generation_time + env_script + before_script
42
+ end
43
+ @init_script
44
+ end
45
+
46
+ def errors
47
+ all_errors = Array.new(@errors)
48
+
49
+ instance_variables.each do |var|
50
+ section = instance_variable_get(var)
51
+ next unless section.respond_to?(:name) && section.respond_to?(:errors)
52
+ section_name = section.send(:name)
53
+ section_errors = section.send(:errors)
54
+ section_errors.each do |err|
55
+ all_errors << "#{section_name}: #{err}"
56
+ end
57
+ end
58
+
59
+ all_errors
60
+ end
61
+
62
+ def inspect
63
+ data = []
64
+ instance_variables.each do |var|
65
+ section = instance_variable_get(var)
66
+ if section.respond_to?(:name)
67
+ section_name = section.send(:name)
68
+ data << "#{section_name}: #{section.inspect}"
69
+ end
70
+ end
71
+ data.join("\n")
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module CHBuild
3
+ class Config
4
+ # Before section
5
+ class Before < Array
6
+ # before_commands is required so no default value
7
+ def initialize(before_commands)
8
+ super([])
9
+ validate!(before_commands)
10
+ replace(before_commands) unless before_commands.nil?
11
+ end
12
+
13
+ def validate!(before_commands)
14
+ @errors = []
15
+ @errors << 'Required' if before_commands.nil?
16
+ @errors << 'Cannot be empty' if before_commands == []
17
+ end
18
+
19
+ attr_reader :errors
20
+
21
+ def name
22
+ "Section 'before'"
23
+ end
24
+
25
+ def to_bash_script
26
+ reduce('') { |a, e| a + "#{e}\n" }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ module CHBuild
3
+ class Config
4
+ # Env section
5
+ class Env < Hash
6
+ def initialize(env = {})
7
+ validate!(env)
8
+ super
9
+ replace(env) unless env.nil?
10
+ end
11
+
12
+ def validate!(_env)
13
+ true
14
+ end
15
+
16
+ def errors
17
+ []
18
+ end
19
+
20
+ def name
21
+ "Section 'env'"
22
+ end
23
+
24
+ def to_bash_script
25
+ reduce('') { |a, (k, v)| a + "export #{k}=\"#{v}\"\n" }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module CHBuild
3
+ class Config
4
+ # NotFoundError
5
+ class NotFoundError < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'chbuild/constants'
4
+
5
+ module CHBuild
6
+ class Config
7
+ # Use section
8
+ class Use < Hash
9
+ DEFAULTS ||= {
10
+ 'php' => CHBuild::DEFAULT_PHP_VERSION,
11
+ 'mysql' => CHBuild::DEFAULT_MYSQL_VERSION
12
+ }.freeze
13
+
14
+ ALLOWED_VALUES ||= DEFAULTS.keys
15
+
16
+ def initialize(using = {})
17
+ super
18
+ replace(DEFAULTS)
19
+
20
+ validate!(using)
21
+
22
+ merge!(using) unless using.nil?
23
+ end
24
+
25
+ def validate!(using)
26
+ @errors = []
27
+ if using.respond_to? :keys
28
+ extra_keys = using.keys - ALLOWED_VALUES
29
+ extra_keys.each do |key|
30
+ @errors << "Unknown key: #{key}"
31
+ end
32
+ end
33
+ end
34
+
35
+ attr_reader :errors
36
+
37
+ def name
38
+ "Section 'use'"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module CHBuild
3
+ class Config
4
+ # Version section
5
+ class Version
6
+ ALLOWED_VALUES ||= [1.0].freeze
7
+
8
+ # version is required so no default value
9
+ def initialize(version)
10
+ validate!(version)
11
+ @yaml_version = version
12
+ end
13
+
14
+ def inspect
15
+ @yaml_version.to_s
16
+ end
17
+
18
+ def validate!(version)
19
+ @errors = []
20
+ if version.nil?
21
+ @errors << 'Required'
22
+ return
23
+ end
24
+ @errors << "Unknown value: '#{version}'" unless ALLOWED_VALUES.include? version
25
+ end
26
+
27
+ attr_reader :errors
28
+
29
+ def name
30
+ "Section 'version'"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # CHBuild main module
6
+ module CHBuild
7
+ GEM_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '../../'))
8
+ TEMPLATE_DIR = "#{CHBuild::GEM_ROOT}/templates"
9
+ DEFAULT_DOMAIN = 'cheppers.com'
10
+
11
+ IMAGE_NAME ||= 'chbuild'
12
+ DOCKER_DIR ||= 'docker'
13
+ DEFAULT_PHP_VERSION = '5.6'
14
+ DEFAULT_MYSQL_VERSION = 'latest'
15
+
16
+ DEFAULT_OPTS ||= {
17
+ refreshed_at_date: ::Date.today.strftime('%Y-%m-%d'),
18
+ php_timezone: 'Europe/Budapest',
19
+ php_memory_limit: '256M',
20
+ max_upload: '50M',
21
+ php_max_file_upload: 200,
22
+ php_max_post: '100M',
23
+ extra_files: CHBuild::TEMPLATE_DIR
24
+ }.freeze
25
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+ require 'docker-api'
3
+ require 'erb'
4
+
5
+ require 'chbuild/bindable_hash'
6
+ require 'chbuild/config'
7
+ require 'chbuild/utils'
8
+
9
+ # rubocop:disable Metrics/ClassLength, Lint/AssignmentInCondition
10
+
11
+ # CHBuild main module
12
+ module CHBuild
13
+ # CHBuild::Controller
14
+ class Controller
15
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
16
+ def self.config(path_to_config = nil)
17
+ # rubocop:disable Style/ClassVars
18
+ @@config ||= nil
19
+
20
+ unless @@config
21
+ @@config = if path_to_config.nil?
22
+ CHBuild::Config.new(File.join(Dir.pwd, '.chbuild.yml'))
23
+ else
24
+ CHBuild::Config.new(path_to_config)
25
+ end
26
+ end
27
+ @@config
28
+ end
29
+
30
+ def self.image_exist?
31
+ Docker::Image.exist? CHBuild::IMAGE_NAME
32
+ end
33
+
34
+ def self.container?
35
+ Docker::Container.all(
36
+ 'filters' => { 'ancestor' => [CHBuild::IMAGE_NAME] }.to_json, 'all' => true
37
+ ).first
38
+ rescue
39
+ nil
40
+ end
41
+
42
+ def self.promote
43
+ puts '!!! WIP !!!'
44
+ puts "FQDN: #{CHBuild::Utils.fqdn}"
45
+ end
46
+
47
+ def self.build
48
+ template = load_docker_template(
49
+ 'base-docker-container',
50
+ php_memory_limit: '128M'
51
+ )
52
+
53
+ docker_context = generate_docker_archive(template)
54
+
55
+ Docker::Image.build_from_tar(docker_context, t: CHBuild::IMAGE_NAME) do |r|
56
+ r.each_line do |log|
57
+ if (message = JSON.parse(log)) && message.key?('stream')
58
+ yield message['stream'] if block_given?
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # rubocop:disable Metrics/PerceivedComplexity, Lint/UnusedMethodArgument, Metrics/MethodLength
65
+ def self.run(config_path: nil, webroot: nil, initscripts: nil)
66
+ unless image_exist?
67
+ yield "Image doesn't exist" if block_given?
68
+ return false
69
+ end
70
+
71
+ bind_volumes = []
72
+
73
+ if !webroot.nil? && Dir.exist?(File.expand_path(webroot))
74
+ bind_volumes << "#{webroot}:/www"
75
+ end
76
+ if !initscripts.nil? && Dir.exist?(File.expand_path(initscripts))
77
+ bind_volumes << "#{initscripts}:/initscripts"
78
+ end
79
+
80
+ if c = container?
81
+ yield 'Removing existing chbuild container...' if block_given?
82
+ c.remove(force: true)
83
+ end
84
+
85
+ mysql_container_version = "mysql:#{config.use['mysql']}"
86
+ unless Docker::Image.exist? mysql_container_version
87
+ yield "Downloading #{mysql_container_version}..." if block_given?
88
+ Docker::Image.create('fromImage' => mysql_container_version)
89
+ end
90
+
91
+ mysql_container_name = mysql_container_version.tr(':.', '_')
92
+
93
+ stop_mysql_containers(except: mysql_container_version)
94
+ begin
95
+ mysql_container = Docker::Container.get(mysql_container_name)
96
+ rescue Docker::Error::NotFoundError
97
+ yield 'Creating MySQL container...' if block_given?
98
+ mysql_container = Docker::Container.create(
99
+ 'name' => mysql_container_name,
100
+ 'Image' => mysql_container_version,
101
+ 'Env' => ['MYSQL_ROOT_PASSWORD=admin']
102
+ )
103
+ ensure
104
+ mysql_container_info = Docker::Container.get(mysql_container.id).info
105
+ unless mysql_container_info['State']['Running']
106
+ yield 'Starting MySQL container...' if block_given?
107
+ mysql_container.start!
108
+ sleep 10
109
+ end
110
+ end
111
+ mysql_container_info = Docker::Container.get(mysql_container.id).info
112
+ unless mysql_container_info['State']['Running']
113
+ yield "Couldn't start MySQL container" if block_given?
114
+ return false
115
+ end
116
+ yield "MySQL container: #{mysql_container.id[0, 10]}" if block_given?
117
+
118
+ yield 'Creating CHBuild container...' if block_given?
119
+ container = Docker::Container.create(
120
+ 'name' => CHBuild::Utils.generate_conatiner_name,
121
+ 'Image' => CHBuild::IMAGE_NAME,
122
+ 'Cmd' => ['/init.sh'],
123
+ 'Volumes' => {
124
+ "#{Dir.pwd}/webroot" => {},
125
+ "#{Dir.pwd}/initscripts" => {}
126
+ },
127
+ 'ExposedPorts' => {
128
+ '80/tcp' => {}
129
+ },
130
+ 'HostConfig' => {
131
+ 'Links' => ["#{mysql_container_name}:mysql"],
132
+ 'PortBindings' => {
133
+ '80/tcp' => [{ 'HostPort' => '8088' }]
134
+ },
135
+ 'Binds' => bind_volumes
136
+ }
137
+ )
138
+
139
+ container.store_file('/init.sh', content: load_init_script, permissions: 0777)
140
+
141
+ container.start!
142
+ end
143
+
144
+ def self.delete_container
145
+ if c = container?
146
+ c.delete(force: true)
147
+ true
148
+ else
149
+ false
150
+ end
151
+ end
152
+
153
+ def self.container_id
154
+ if c = container?
155
+ c.id[0, 10]
156
+ end
157
+ end
158
+
159
+ def self.container_logs
160
+ if c = container?
161
+ c.logs(stdout: true)
162
+ end
163
+ end
164
+
165
+ def self.delete_image
166
+ if image_exist?
167
+ image = Docker::Image.get CHBuild::IMAGE_NAME
168
+ image.remove(force: true)
169
+ end
170
+ end
171
+
172
+ def self.mysql_containers
173
+ Docker::Container.all.select { |c| c.info['Image'].start_with?('mysql') }
174
+ end
175
+
176
+ def self.delete_mysql_containers
177
+ if mysql_containers.empty?
178
+ false
179
+ else
180
+ mysql_containers.each { |c| c.delete(force: true) }
181
+ true
182
+ end
183
+ end
184
+
185
+ def self.stop_mysql_containers(except: "\n")
186
+ mysql_containers.select { |c| !c.info['Image'].end_with?(except) }.each(&:stop)
187
+ end
188
+
189
+ private_class_method
190
+
191
+ def self.load_docker_template(template_name, opts = {})
192
+ opts = CHBuild::DEFAULT_OPTS.merge(opts)
193
+
194
+ context = BindableHash.new opts
195
+ ::ERB.new(
196
+ File.read("#{CHBuild::TEMPLATE_DIR}/#{template_name}.erb")
197
+ ).result(context.get_binding)
198
+ end
199
+
200
+ def self.load_init_script
201
+ context = BindableHash.new(before_script: config.init_script)
202
+ ::ERB.new(
203
+ File.read("#{CHBuild::TEMPLATE_DIR}/init.sh.erb")
204
+ ).result(context.get_binding)
205
+ end
206
+
207
+ def self.generate_docker_archive(dockerfile_content)
208
+ tar = StringIO.new
209
+
210
+ Gem::Package::TarWriter.new(tar) do |writer|
211
+ writer.add_file('Dockerfile', 0644) { |f| f.write(dockerfile_content) }
212
+ end
213
+
214
+ compress_archive(tar)
215
+ end
216
+
217
+ def self.compress_archive(tar)
218
+ tar.seek(0)
219
+ # rubocop:disable Style/EmptyLiteral
220
+ gz = StringIO.new(String.new, 'r+b').set_encoding(Encoding::BINARY)
221
+ gz_writer = Zlib::GzipWriter.new(gz)
222
+ gz_writer.write(tar.read)
223
+ tar.close
224
+ gz_writer.finish
225
+ gz.rewind
226
+
227
+ gz
228
+ end
229
+ end
230
+ end