contur 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'contur/constants'
4
+
5
+ module Contur
6
+ class Config
7
+ # Use section
8
+ class Use < Hash
9
+ DEFAULTS ||= {
10
+ 'php' => Contur::DEFAULT_PHP_VERSION,
11
+ 'mysql' => Contur::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
+ return unless 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
+
34
+ attr_reader :errors
35
+
36
+ def name
37
+ "Section 'use'"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module Contur
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,74 @@
1
+ # frozen_string_literal: true
2
+ require 'contur/config/before'
3
+ require 'contur/config/env'
4
+ require 'contur/config/errors'
5
+ require 'contur/config/use'
6
+ require 'contur/config/version'
7
+
8
+ require 'yaml'
9
+
10
+ module Contur
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 Contur::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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # Contur main module
6
+ module Contur
7
+ GEM_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '../../'))
8
+ TEMPLATE_DIR = "#{Contur::GEM_ROOT}/templates"
9
+ DEFAULT_DOMAIN = 'cheppers.com'
10
+
11
+ IMAGE_NAME ||= 'contur'
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: Contur::TEMPLATE_DIR
24
+ }.freeze
25
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+ require 'docker-api'
3
+ require 'erb'
4
+
5
+ require 'contur/bindable_hash'
6
+ require 'contur/config'
7
+ require 'contur/utils'
8
+
9
+ # rubocop:disable Metrics/ClassLength, Lint/AssignmentInCondition
10
+
11
+ # Contur main module
12
+ module Contur
13
+ # Contur::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
+ config_file_name = Dir['.contur.y*ml'].first
23
+ Contur::Config.new(File.join(Dir.pwd, config_file_name))
24
+ else
25
+ Contur::Config.new(path_to_config)
26
+ end
27
+ end
28
+ @@config
29
+ end
30
+
31
+ def self.image_exist?
32
+ Docker::Image.exist? Contur::IMAGE_NAME
33
+ end
34
+
35
+ def self.container?
36
+ Docker::Container.all(
37
+ 'filters' => { 'ancestor' => [Contur::IMAGE_NAME] }.to_json, 'all' => true
38
+ ).first
39
+ rescue
40
+ nil
41
+ end
42
+
43
+ def self.promote
44
+ puts '!!! WIP !!!'
45
+ puts "FQDN: #{Contur::Utils.fqdn}"
46
+ end
47
+
48
+ def self.build
49
+ template = load_docker_template(
50
+ 'base-docker-container',
51
+ php_memory_limit: '128M'
52
+ )
53
+
54
+ docker_context = generate_docker_archive(template)
55
+
56
+ Docker::Image.build_from_tar(docker_context, t: Contur::IMAGE_NAME) do |r|
57
+ r.each_line do |log|
58
+ if (message = JSON.parse(log)) && message.key?('stream')
59
+ yield message['stream'] if block_given?
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # rubocop:disable Metrics/PerceivedComplexity, Lint/UnusedMethodArgument, Metrics/MethodLength
66
+ def self.run(config_path: nil, webroot: nil, initscripts: nil)
67
+ unless image_exist?
68
+ yield "Image doesn't exist" if block_given?
69
+ return false
70
+ end
71
+
72
+ bind_volumes = []
73
+
74
+ if !webroot.nil? && Dir.exist?(File.expand_path(webroot))
75
+ bind_volumes << "#{webroot}:/www"
76
+ end
77
+ if !initscripts.nil? && Dir.exist?(File.expand_path(initscripts))
78
+ bind_volumes << "#{initscripts}:/initscripts"
79
+ end
80
+
81
+ if c = container?
82
+ yield 'Removing existing contur container...' if block_given?
83
+ c.remove(force: true)
84
+ end
85
+
86
+ mysql_container_version = "mysql:#{config.use['mysql']}"
87
+ unless Docker::Image.exist? mysql_container_version
88
+ yield "Downloading #{mysql_container_version}..." if block_given?
89
+ Docker::Image.create('fromImage' => mysql_container_version)
90
+ end
91
+
92
+ mysql_container_name = mysql_container_version.tr(':.', '_')
93
+
94
+ stop_mysql_containers(except: mysql_container_version)
95
+ begin
96
+ mysql_container = Docker::Container.get(mysql_container_name)
97
+ rescue Docker::Error::NotFoundError
98
+ yield 'Creating MySQL container...' if block_given?
99
+ mysql_container = Docker::Container.create(
100
+ 'name' => mysql_container_name,
101
+ 'Image' => mysql_container_version,
102
+ 'Env' => ['MYSQL_ROOT_PASSWORD=admin']
103
+ )
104
+ ensure
105
+ mysql_container_info = Docker::Container.get(mysql_container.id).info
106
+ unless mysql_container_info['State']['Running']
107
+ yield 'Starting MySQL container...' if block_given?
108
+ mysql_container.start!
109
+ sleep 10
110
+ end
111
+ end
112
+ mysql_container_info = Docker::Container.get(mysql_container.id).info
113
+ unless mysql_container_info['State']['Running']
114
+ yield "Couldn't start MySQL container" if block_given?
115
+ return false
116
+ end
117
+ yield "MySQL container: #{mysql_container.id[0, 10]}" if block_given?
118
+
119
+ yield 'Creating Contur container...' if block_given?
120
+ container = Docker::Container.create(
121
+ 'name' => Contur::Utils.generate_conatiner_name,
122
+ 'Image' => Contur::IMAGE_NAME,
123
+ 'Cmd' => ['/init.sh'],
124
+ 'Volumes' => {
125
+ "#{Dir.pwd}/webroot" => {},
126
+ "#{Dir.pwd}/initscripts" => {}
127
+ },
128
+ 'ExposedPorts' => {
129
+ '80/tcp' => {}
130
+ },
131
+ 'HostConfig' => {
132
+ 'Links' => ["#{mysql_container_name}:mysql"],
133
+ 'PortBindings' => {
134
+ '80/tcp' => [{ 'HostPort' => '8088' }]
135
+ },
136
+ 'Binds' => bind_volumes
137
+ }
138
+ )
139
+
140
+ container.store_file('/init.sh', content: load_init_script, permissions: 0o777)
141
+
142
+ container.start!
143
+ end
144
+
145
+ def self.delete_container
146
+ if c = container?
147
+ c.delete(force: true)
148
+ true
149
+ else
150
+ false
151
+ end
152
+ end
153
+
154
+ def self.container_id
155
+ return nil unless c = container?
156
+ c.id[0, 10]
157
+ end
158
+
159
+ def self.container_logs
160
+ return nil unless c = container?
161
+ c.logs(stdout: true)
162
+ end
163
+
164
+ def self.delete_image
165
+ return nil unless image_exist?
166
+ image = Docker::Image.get Contur::IMAGE_NAME
167
+ image.remove(force: true)
168
+ end
169
+
170
+ def self.mysql_containers
171
+ Docker::Container.all.select { |c| c.info['Image'].start_with?('mysql') }
172
+ end
173
+
174
+ def self.delete_mysql_containers
175
+ if mysql_containers.empty?
176
+ false
177
+ else
178
+ mysql_containers.each { |c| c.delete(force: true) }
179
+ true
180
+ end
181
+ end
182
+
183
+ def self.stop_mysql_containers(except: "\n")
184
+ mysql_containers.select { |c| !c.info['Image'].end_with?(except) }.each(&:stop)
185
+ end
186
+
187
+ private_class_method
188
+
189
+ def self.load_docker_template(template_name, opts = {})
190
+ opts = Contur::DEFAULT_OPTS.merge(opts)
191
+
192
+ context = BindableHash.new opts
193
+ ::ERB.new(
194
+ File.read("#{Contur::TEMPLATE_DIR}/#{template_name}.erb")
195
+ ).result(context.get_binding)
196
+ end
197
+
198
+ def self.load_init_script
199
+ context = BindableHash.new(before_script: config.init_script)
200
+ ::ERB.new(
201
+ File.read("#{Contur::TEMPLATE_DIR}/init.sh.erb")
202
+ ).result(context.get_binding)
203
+ end
204
+
205
+ def self.generate_docker_archive(dockerfile_content)
206
+ tar = StringIO.new
207
+
208
+ Gem::Package::TarWriter.new(tar) do |writer|
209
+ writer.add_file('Dockerfile', 0o644) { |f| f.write(dockerfile_content) }
210
+ end
211
+
212
+ compress_archive(tar)
213
+ end
214
+
215
+ def self.compress_archive(tar)
216
+ tar.seek(0)
217
+ gz = StringIO.new(String.new, 'r+b').set_encoding(Encoding::BINARY)
218
+ gz_writer = Zlib::GzipWriter.new(gz)
219
+ gz_writer.write(tar.read)
220
+ tar.close
221
+ gz_writer.finish
222
+ gz.rewind
223
+
224
+ gz
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ module Contur
3
+ # Utils class
4
+ class Utils
5
+ def self.generate_conatiner_name(prefix: nil, job_name: nil, build_id: nil)
6
+ prefix ||= ENV['CONTAINER_PREFIX']
7
+ prefix = "#{prefix}-" unless prefix.nil? || prefix.empty?
8
+
9
+ job_name ||= ENV['JOB_NAME'] || random_string
10
+ build_id ||= ENV['BUILD_ID'] || "r#{Random.rand(1000)}"
11
+
12
+ "#{prefix}#{job_name}-#{build_id}"
13
+ end
14
+
15
+ def self.virtual_hostname(prefix: nil, job_name: nil, branch_name: nil)
16
+ prefix ||= ENV['HOST_PREFIX']
17
+ prefix = "#{prefix}." unless prefix.nil? || prefix.empty?
18
+
19
+ job_name ||= ENV['JOB_NAME'] || random_string.downcase
20
+ branch_name ||= (ENV['BRANCH_NAME'] || 'master').gsub(%r{.*/}, '')
21
+
22
+ "#{prefix}#{branch_name}.#{job_name}"
23
+ end
24
+
25
+ def self.fqdn(prefix: nil, job_name: nil, branch_name: nil)
26
+ vhost = virtual_hostname(
27
+ prefix: prefix,
28
+ job_name: job_name,
29
+ branch_name: branch_name
30
+ )
31
+
32
+ main_domain = ENV['DOMAIN'] || Contur::DEFAULT_DOMAIN
33
+
34
+ "#{vhost}.#{main_domain}"
35
+ end
36
+
37
+ def self.random_string(length: 8)
38
+ o = [('a'..'z'), ('A'..'Z')].map(&:to_a).flatten
39
+ (0...length).map { o[rand(o.length)] }.join
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Contur
3
+ VERSION = '0.0.3'
4
+ end
data/lib/contur.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'contur/version'
3
+
4
+ # require application files
5
+ require_relative 'contur/bindable_hash'
6
+ require_relative 'contur/constants'
7
+ require_relative 'contur/controller'
8
+ require_relative 'contur/config'
@@ -0,0 +1,104 @@
1
+ FROM alpine:3.3
2
+
3
+ MAINTAINER 'Cheppers Ltd. <info@cheppers.com> (github:Cheppers)'
4
+
5
+ ENV REFRESHED_AT <%= refreshed_at_date %>
6
+
7
+ # PHP Configuration
8
+ ENV TIMEZONE <%= php_timezone %>
9
+ ENV PHP_MEMORY_LIMIT <%= php_memory_limit %>
10
+ ENV MAX_UPLOAD <%= max_upload %>
11
+ ENV PHP_MAX_FILE_UPLOAD <%= php_max_file_upload %>
12
+ ENV PHP_MAX_POST <%= php_max_post %>
13
+ ENV TERM dumb # for mysql-client
14
+
15
+ # Install packages
16
+ RUN apk add --no-cache \
17
+ bash \
18
+ apache2-proxy apache2-utils \
19
+ mysql-client \
20
+ tzdata \
21
+ php-mcrypt \
22
+ php-soap \
23
+ php-openssl \
24
+ php-gmp \
25
+ php-pdo_odbc \
26
+ php-json \
27
+ php-dom \
28
+ php-pdo \
29
+ php-zip \
30
+ php-mysql \
31
+ php-sqlite3 \
32
+ php-apcu \
33
+ php-pdo_pgsql \
34
+ php-bcmath \
35
+ php-gd \
36
+ php-xcache \
37
+ php-odbc \
38
+ php-pdo_mysql \
39
+ php-pdo_sqlite \
40
+ php-gettext \
41
+ php-xmlreader \
42
+ php-xmlrpc \
43
+ php-bz2 \
44
+ php-memcache \
45
+ php-mssql \
46
+ php-iconv \
47
+ php-pdo_dblib \
48
+ php-curl \
49
+ php-ctype \
50
+ php-phar \
51
+ php-fpm
52
+
53
+ # Configure apache proxy (apache because of `.ht` files)
54
+ RUN echo \
55
+ 'ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/www/$1' >> /etc/apache2/httpd.conf && \
56
+ echo 'DirectoryIndex /index.php' >> /etc/apache2/httpd.conf && \
57
+ sed -ie 's/#ServerName .*/ServerName localhost/i' /etc/apache2/httpd.conf && \
58
+ sed -ie 's/#LoadModule slotmem_shm_module/LoadModule slotmem_shm_module/' \
59
+ /etc/apache2/httpd.conf && \
60
+ sed -ie 's#/var/www/localhost/htdocs#/www#g' /etc/apache2/httpd.conf && \
61
+ sed -ie 's#LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so##g' /etc/apache2/conf.d/proxy.conf && \
62
+ mkdir /run/apache2/
63
+
64
+ # Configure Timezone, important for php
65
+ RUN cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \
66
+ echo "${TIMEZONE}" > /etc/timezone
67
+
68
+ # Configure PHP-FPM
69
+ RUN sed -i "s|;*daemonize\s*=\s*yes|daemonize = no|g" /etc/php/php-fpm.conf && \
70
+ sed -ie 's#^listen\s*=\s*.*#listen = 9000#g' \
71
+ /etc/php/php-fpm.conf && \
72
+ sed -i "s|;*date.timezone =.*|date.timezone = ${TIMEZONE}|i" \
73
+ /etc/php/php.ini && \
74
+ sed -i "s|;*memory_limit =.*|memory_limit = ${PHP_MEMORY_LIMIT}|i" \
75
+ /etc/php/php.ini && \
76
+ sed -i "s|;*upload_max_filesize =.*|upload_max_filesize = ${MAX_UPLOAD}|i" \
77
+ /etc/php/php.ini && \
78
+ sed -i "s|;*max_file_uploads =.*|max_file_uploads = ${PHP_MAX_FILE_UPLOAD}|i" \
79
+ /etc/php/php.ini && \
80
+ sed -i "s|;*post_max_size =.*|post_max_size = ${PHP_MAX_POST}|i" \
81
+ /etc/php/php.ini && \
82
+ sed -i "s|;*cgi.fix_pathinfo=.*|cgi.fix_pathinfo= 0|i" /etc/php/php.ini
83
+
84
+ # Install Composer
85
+ RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
86
+ php -r "if (hash_file('SHA384', 'composer-setup.php') === 'e115a8dc7871f15d853148a7fbac7da27d6c0030b848d9b3dc09e2a0388afed865e6a3d6b3c0fad45c48e2b5fc1196ae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" && \
87
+ php composer-setup.php --install-dir /usr/bin --filename=composer composer && \
88
+ php -r "unlink('composer-setup.php');" && \
89
+ chmod +x /usr/bin/composer
90
+
91
+ # Install Drush
92
+ RUN php -r "readfile('http://files.drush.org/drush.phar');" > /usr/local/bin/drush && \
93
+ php /usr/local/bin/drush core-status && \
94
+ chmod +x /usr/local/bin/drush
95
+
96
+ # Codebase
97
+ RUN mkdir /www
98
+ VOLUME ["/www"]
99
+
100
+ RUN mkdir /initscripts
101
+
102
+ WORKDIR /www
103
+
104
+ EXPOSE 80
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+
3
+ # ========== GENERATED SCRIPT ==========
4
+ <%= before_script %>
5
+ # ========== GENERATED SCRIPT ==========
6
+
7
+ echo "Run InitScripts"
8
+ find /initscripts -type f -print -exec {} \;
9
+
10
+ echo "Start Apache2"
11
+ /usr/sbin/httpd -d . -f /etc/apache2/httpd.conf
12
+
13
+ if [ ! -f /www/index.php ]; then
14
+ echo "Populate '/www' because it's empty"
15
+ echo '<?php phpinfo(); ?>' > /www/index.php
16
+ fi
17
+
18
+ echo "Start PHP-FPM"
19
+ /usr/bin/php-fpm