dapp 0.0.1

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/lib/dapp/cli.rb ADDED
@@ -0,0 +1,133 @@
1
+ require 'mixlib/cli'
2
+
3
+ module Dapp
4
+ # CLI
5
+ class CLI
6
+ include Mixlib::CLI
7
+
8
+ banner <<BANNER.freeze
9
+ Version: #{Dapp::VERSION}
10
+
11
+ Usage:
12
+ dappit [options] [PATTERN ...]
13
+
14
+ PATTERN Applications to process [default: *].
15
+
16
+ Options:
17
+ BANNER
18
+
19
+ option :version,
20
+ long: '--version',
21
+ description: 'Show version',
22
+ on: :tail,
23
+ boolean: true,
24
+ proc: proc { puts "Version: #{Dapp::VERSION}" },
25
+ exit: 0
26
+
27
+ option :quiet,
28
+ short: '-q',
29
+ long: '--quiet',
30
+ description: 'Suppress logging',
31
+ on: :tail,
32
+ boolean: true,
33
+ proc: proc { Dapp::Builder.default_opts[:log_quiet] = true }
34
+
35
+ option :verbose,
36
+ long: '--verbose',
37
+ description: 'Enable verbose output',
38
+ on: :tail,
39
+ boolean: true,
40
+ proc: proc { Dapp::Builder.default_opts[:log_verbose] = true }
41
+
42
+ option :help,
43
+ short: '-h',
44
+ long: '--help',
45
+ description: 'Show this message',
46
+ on: :tail,
47
+ boolean: true,
48
+ show_options: true,
49
+ exit: 0
50
+
51
+ option :build_dir,
52
+ long: '--build-dir PATH',
53
+ description: 'Build directory',
54
+ proc: proc { |p| Dapp::Builder.default_opts[:build_dir] = p }
55
+
56
+ option :dir,
57
+ long: '--dir PATH',
58
+ description: 'Change to directory',
59
+ on: :head
60
+
61
+ option :dappfile_name,
62
+ long: '--dappfile-name NAME',
63
+ description: 'Name of Dappfile',
64
+ proc: proc { |n| Dapp::Builder.default_opts[:dappfile_name] = n },
65
+ on: :head
66
+
67
+ option :flush_cache,
68
+ long: '--flush-cache',
69
+ description: 'Flush cache',
70
+ boolean: true,
71
+ proc: proc { Dapp::Builder.default_opts[:flush_cache] = true }
72
+
73
+ option :docker_registry,
74
+ long: '--docker-registry REGISTRY',
75
+ description: 'Docker registry',
76
+ proc: proc { |r| Dapp::Builder.default_opts[:docker_registry] = r }
77
+
78
+ option :cascade_tagging,
79
+ long: '--cascade_tagging',
80
+ description: 'Use cascade tagging',
81
+ boolean: true,
82
+ proc: proc { Dapp::Builder.default_opts[:cascade_tagging] = true }
83
+
84
+ option :git_artifact_branch,
85
+ long: '--git-artifact-branch BRANCH',
86
+ description: 'Default branch to archive artifacts from',
87
+ proc: proc { |b| Dapp::Builder.default_opts[:git_artifact_branch] = b }
88
+
89
+ def dappfile_path
90
+ @dappfile_path ||= File.join [config[:dir], 'Dappfile'].compact
91
+ end
92
+
93
+ def patterns
94
+ @patterns ||= cli_arguments
95
+ end
96
+
97
+ def run(argv = ARGV)
98
+ begin
99
+ parse_options(argv)
100
+ rescue OptionParser::InvalidOption => e
101
+ STDERR.puts "Error: #{e.message}"
102
+ exit 1
103
+ end
104
+
105
+ patterns << '*' unless patterns.any?
106
+
107
+ if File.exist? dappfile_path
108
+ process_file
109
+ else
110
+ process_directory
111
+ end
112
+ end
113
+
114
+ def process_file
115
+ patterns.each do |pattern|
116
+ unless Dapp::Builder.process_file(dappfile_path, app_filter: pattern).builded_apps.any?
117
+ STDERR.puts "Error! No such app: '#{pattern}' in #{dappfile_path}"
118
+ exit 1
119
+ end
120
+ end
121
+ end
122
+
123
+ def process_directory
124
+ Dapp::Builder.default_opts[:shared_build_dir] = true
125
+ patterns.each do |pattern|
126
+ unless Dapp::Builder.process_directory(config[:dir], pattern).any?
127
+ STDERR.puts "Error! No such app '#{pattern}'"
128
+ exit 1
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,188 @@
1
+ module Dapp
2
+ # Dockerfile builder and docker binary wrapper
3
+ class Docker
4
+ def initialize(builder)
5
+ @builder = builder
6
+ end
7
+
8
+ def name(name = nil)
9
+ @name = name || @name
10
+ end
11
+
12
+ def from(from = nil)
13
+ @from = from || @from
14
+ end
15
+
16
+ def build_path(*path)
17
+ builder.build_path(builder.home_branch, [name, 'docker'].compact.join('.'), *path)
18
+ end
19
+
20
+ def run(*command, step: :build)
21
+ add_instruction step, :run, command.join(' && ')
22
+ end
23
+
24
+ def workdir(directory, step: :build)
25
+ add_instruction step, :workdir, directory
26
+ end
27
+
28
+ def copy(what, where, step: :build)
29
+ add_instruction step, :copy, what, where
30
+ end
31
+
32
+ def add(what, where, step: :build)
33
+ add_instruction step, :add, what, where
34
+ end
35
+
36
+ def add_artifact(file_path, filename, where, step: :build)
37
+ add_instruction step, :add_artifact, file_path, filename, where
38
+ end
39
+
40
+ def expose(*ports, step: :build)
41
+ add_instruction step, :expose, ports
42
+ end
43
+
44
+ def env(step: :build, **env)
45
+ add_instruction step, :env, env
46
+ end
47
+
48
+ def volume(*paths, step: :build)
49
+ add_instruction step, :volume, paths
50
+ end
51
+
52
+ def cmd(*commands, step: :build)
53
+ add_instruction step, :cmd, commands
54
+ end
55
+
56
+ def initialize_dup(other)
57
+ super
58
+
59
+ @name = @name.dup if @name
60
+ @from = @from.dup if @from
61
+
62
+ @instructions = @instructions.dup
63
+ @instructions.each do |step, step_instructions|
64
+ @instructions[step] = step_instructions.dup
65
+ end
66
+ end
67
+
68
+ def build
69
+ # prepare
70
+ generate_dockerfile
71
+
72
+ # build
73
+ res = docker("build --force-rm=true --rm=true #{build_path}", log_verbose: true)
74
+
75
+ # return image id
76
+ res.stdout.lines.grep(/^Successfully built ([0-9a-f]+)\n$/).first.strip.split.last
77
+ end
78
+
79
+ def image_exists?(**kwargs)
80
+ !image_id(**kwargs).nil?
81
+ end
82
+
83
+ def image_id(**kwargs)
84
+ image = images(**kwargs).first
85
+ image ? image[:id] : nil
86
+ end
87
+
88
+ def images(name:, tag: nil, registry: nil)
89
+ all_images.select { |i| i[:name] == pad_image_name(name: name, registry: registry) && (!tag || i[:tag] == tag) }
90
+ end
91
+
92
+ def all_images
93
+ docker('images').stdout.lines.drop(1).map(&:strip)
94
+ .map { |line| Hash[[:name, :tag, :id].zip(line.strip.split(/\s{2,}/)[0..3])] }
95
+ end
96
+
97
+ def tag(origin, new, force: true)
98
+ docker "tag#{' -f' if force} #{origin.is_a?(String) ? origin : pad_image_name(**origin)} #{pad_image_name(**new)}"
99
+ end
100
+
101
+ def rmi(**kwargs)
102
+ docker "rmi #{pad_image_name(**kwargs)}"
103
+ end
104
+
105
+ def push(name:, tag: nil, registry: nil)
106
+ docker "push #{pad_image_name name: name, tag: tag, registry: registry}", log_verbose: true
107
+ end
108
+
109
+ protected
110
+
111
+ def pad_image_name(name:, tag: nil, registry: nil)
112
+ name = "#{registry}/#{name}" if registry
113
+ name = "#{name}:#{tag}" if tag
114
+ name
115
+ end
116
+
117
+ attr_reader :builder
118
+
119
+ def instructions(step)
120
+ (@instructions ||= {})[step] ||= []
121
+ end
122
+
123
+ def add_instruction(step, *args)
124
+ instructions(step) << args
125
+ end
126
+
127
+ def docker(command, **kwargs)
128
+ builder.shellout "docker #{command}", **kwargs
129
+ end
130
+
131
+ def dockerfile_path
132
+ build_path 'Dockerfile'
133
+ end
134
+
135
+ def generate_dockerfile_run(dockerfile, *params)
136
+ dockerfile.puts 'RUN ' + params[0]
137
+ end
138
+
139
+ def generate_dockerfile_copy(dockerfile, *params)
140
+ dockerfile.puts "COPY #{params[0]} #{params[1]}"
141
+ end
142
+
143
+ def generate_dockerfile_add(dockerfile, *params)
144
+ dockerfile.puts "ADD #{params[0]} #{params[1]}"
145
+ end
146
+
147
+ def generate_dockerfile_add_artifact(dockerfile, *params)
148
+ FileUtils.link params[0], build_path(params[1]), force: true
149
+ dockerfile.puts "ADD #{params[1]} #{params[2]}"
150
+ end
151
+
152
+ def generate_dockerfile_expose(dockerfile, *params)
153
+ dockerfile.puts 'EXPOSE ' + params[0].map(&:to_s).join(' ')
154
+ end
155
+
156
+ def generate_dockerfile_env(dockerfile, *params)
157
+ dockerfile.puts 'ENV ' + params[0].map { |k, v| %(#{k}="#{v}") }.join(' ')
158
+ end
159
+
160
+ def generate_dockerfile_volume(dockerfile, *params)
161
+ dockerfile.puts 'VOLUME ' + params[0].join(' ')
162
+ end
163
+
164
+ def generate_dockerfile_workdir(dockerfile, *params)
165
+ dockerfile.puts "WORKDIR #{params[0]}"
166
+ end
167
+
168
+ def generate_dockerfile_cmd(dockerfile, *params)
169
+ dockerfile.puts 'CMD ' + params[0].join(' ')
170
+ end
171
+
172
+ def generate_dockerfile_step(dockerfile, step)
173
+ instructions(step).each do |directive, *params|
174
+ send :"generate_dockerfile_#{directive}", dockerfile, *params
175
+ end
176
+ end
177
+
178
+ def generate_dockerfile
179
+ File.open dockerfile_path, 'w' do |dockerfile|
180
+ dockerfile.puts 'FROM ' + from
181
+
182
+ [:begining, :prepare, :build, :setup].each do |step|
183
+ generate_dockerfile_step(dockerfile, step)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,42 @@
1
+ module Dapp
2
+ # File Monitor
3
+ module Filelock
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # ClassMethods
9
+ module ClassMethods
10
+ def filelocks
11
+ @filelocks ||= Hash.new(false)
12
+ end
13
+ end
14
+
15
+ def filelock(filelock, error_message: 'Already in use!', timeout: 10, &blk)
16
+ return yield if self.class.filelocks[filelock]
17
+
18
+ begin
19
+ self.class.filelocks[filelock] = true
20
+ filelock_lockfile(filelock, error_message: error_message, timeout: timeout, &blk)
21
+ yield
22
+ ensure
23
+ self.class.filelocks[filelock] = false
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def filelock_lockfile(filelock, error_message: 'Already in use!', timeout: 10)
30
+ File.open(build_path(filelock), File::RDWR | File::CREAT, 0644) do |file|
31
+ Timeout.timeout(timeout) do
32
+ file.flock(File::LOCK_EX)
33
+ end
34
+
35
+ yield
36
+ end
37
+ rescue Timeout::Error
38
+ STDERR.puts error_message
39
+ exit 1
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,330 @@
1
+ module Dapp
2
+ # Artifact from Git repo
3
+ class GitArtifact
4
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
5
+ def initialize(builder, repo, where_to_add, name: nil, branch: 'master', cwd: nil, paths: nil, owner: nil, group: nil,
6
+ interlayer_period: 7 * 24 * 3600, build_path: nil, flush_cache: false)
7
+ @builder = builder
8
+ @repo = repo
9
+ @name = name
10
+
11
+ @where_to_add = where_to_add
12
+ @branch = branch
13
+ @cwd = cwd
14
+ @paths = paths
15
+ @owner = owner
16
+ @group = group
17
+
18
+ @interlayer_period = interlayer_period
19
+
20
+ @build_path = build_path || []
21
+
22
+ @atomizer = Atomizer.new builder, build_path(filename('.atomizer'))
23
+
24
+ # check params hash
25
+ lock do
26
+ cleanup! unless !flush_cache && File.exist?(paramshash_path) && File.read(paramshash_path) == paramshash
27
+ File.write paramshash_path, paramshash
28
+ end
29
+ end
30
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
31
+
32
+ def build_path(*paths)
33
+ builder.build_path(*@build_path, *paths)
34
+ end
35
+
36
+ def add_multilayer!
37
+ lock_with_repo do
38
+ # create and add archive
39
+ create_and_add_archive
40
+ return if archive_commit == repo_latest_commit
41
+
42
+ # add layer patches
43
+ latest_layer = add_layer_patches
44
+ if latest_layer
45
+ latest_layer_commit = layer_commit(latest_layer)
46
+ return if latest_layer_commit == repo_latest_commit
47
+ end
48
+
49
+ # empty changes
50
+ unless any_changes?(latest_layer_commit || archive_commit)
51
+ remove_latest!
52
+ return
53
+ end
54
+
55
+ # create and add last patch
56
+ create_and_add_last_patch(latest_layer, latest_layer_commit)
57
+ end
58
+ end
59
+
60
+ def cleanup!
61
+ lock do
62
+ FileUtils.rm_f [
63
+ paramshash_path,
64
+ archive_path,
65
+ archive_commitfile_path,
66
+ Dir.glob(layer_patch_path('*')),
67
+ Dir.glob(layer_commitfile_path('*')),
68
+ latest_patch_path,
69
+ latest_commitfile_path
70
+ ].flatten
71
+ end
72
+ end
73
+
74
+ def exists_in_commit?(path, commit)
75
+ repo.git_bare("cat-file -e #{commit}:#{path}", returns: [0, 128]).status.success?
76
+ end
77
+
78
+ def exists_in_step?(path, step)
79
+ exists_in_commit?(path, commit_by_step(step))
80
+ end
81
+
82
+ def prepare_step_commit
83
+ archive_commit
84
+ end
85
+
86
+ def build_step_commit
87
+ layer_commit(layers.last) || archive_commit
88
+ end
89
+
90
+ def setup_step_commit
91
+ latest_commit || layer_commit(layers.last) || archive_commit
92
+ end
93
+
94
+ def commit_by_step(step)
95
+ send :"#{step}_step_commit"
96
+ end
97
+
98
+ def any_changes?(from)
99
+ !repo.git_bare("diff --quiet #{from}..#{repo_latest_commit}#{" --relative=#{cwd}" if cwd} #{paths(true)}", returns: [0, 1]).status.success?
100
+ end
101
+
102
+ protected
103
+
104
+ attr_reader :builder
105
+ attr_reader :repo
106
+ attr_reader :name
107
+ attr_reader :where_to_add
108
+ attr_reader :branch
109
+ attr_reader :cwd
110
+ attr_reader :owner
111
+ attr_reader :group
112
+ attr_reader :interlayer_period
113
+ attr_reader :atomizer
114
+
115
+ def lock_with_repo(&blk)
116
+ lock do
117
+ repo.lock(&blk)
118
+ end
119
+ end
120
+
121
+ def create_and_add_archive
122
+ create_archive! unless archive_exists?
123
+ add_archive
124
+ end
125
+
126
+ def add_layer_patches
127
+ latest_layer = nil
128
+ layers.each do |layer|
129
+ add_layer_patch layer
130
+ latest_layer = layer
131
+ end
132
+
133
+ latest_layer
134
+ end
135
+
136
+ def create_and_add_last_patch_as_layer_patch(latest_layer, latest_layer_commit)
137
+ remove_latest!
138
+ layer = latest_layer.to_i + 1
139
+ create_layer_patch!(latest_layer_commit || archive_commit, layer)
140
+ add_layer_patch layer
141
+ end
142
+
143
+ def create_and_add_last_patch_as_latest_patch(_latest_layer, latest_layer_commit)
144
+ if latest_commit != repo_latest_commit
145
+ create_latest_patch!(latest_layer_commit || archive_commit)
146
+ end
147
+ add_latest_patch
148
+ end
149
+
150
+ def create_and_add_last_patch(latest_layer, latest_layer_commit)
151
+ if (Time.now - repo.commit_at(latest_layer_commit || archive_commit)) > interlayer_period
152
+ create_and_add_last_patch_as_layer_patch(latest_layer, latest_layer_commit)
153
+ else
154
+ create_and_add_last_patch_as_latest_patch(latest_layer, latest_layer_commit)
155
+ end
156
+ end
157
+
158
+ def paths(with_cwd = false)
159
+ [@paths].flatten.compact.map { |path| (with_cwd && cwd ? "#{cwd}/#{path}" : path).gsub(%r{^\/*|\/*$}, '') }.join(' ') if @paths
160
+ end
161
+
162
+ def repo_latest_commit
163
+ repo.latest_commit(branch)
164
+ end
165
+
166
+ def filename(ending)
167
+ "#{repo.name}#{name ? "_#{name}" : nil}.#{branch}#{ending}"
168
+ end
169
+
170
+ def paramshash_filename
171
+ filename '.paramshash'
172
+ end
173
+
174
+ def paramshash_path
175
+ build_path paramshash_filename
176
+ end
177
+
178
+ def paramshash
179
+ Digest::SHA256.hexdigest [cwd, paths, owner, group].map(&:to_s).join(':::')
180
+ end
181
+
182
+ def archive_filename
183
+ filename '.tar.gz'
184
+ end
185
+
186
+ def archive_path
187
+ build_path archive_filename
188
+ end
189
+
190
+ def archive_commitfile_path
191
+ build_path filename '.commit'
192
+ end
193
+
194
+ def archive_commit
195
+ File.read archive_commitfile_path
196
+ end
197
+
198
+ def create_arhive_with_owner_substitution!
199
+ Dir.mktmpdir('change_archive_owner', build_path) do |tmpdir_path|
200
+ atomizer << tmpdir_path
201
+ repo.git_bare "archive #{repo_latest_commit}:#{cwd} #{paths} | /bin/tar --extract --directory #{tmpdir_path}"
202
+ builder.shellout("/usr/bin/find #{tmpdir_path} -maxdepth 1 -mindepth 1 -printf '%P\\n' | /bin/tar -czf #{archive_path} -C #{tmpdir_path}" \
203
+ " -T - --owner=#{owner || 'root'} --group=#{group || 'root'}")
204
+ end
205
+ end
206
+
207
+ def create_simple_archive!
208
+ repo.git_bare "archive --format tar.gz #{repo_latest_commit}:#{cwd} -o #{archive_path} #{paths}"
209
+ end
210
+
211
+ def create_archive!
212
+ atomizer << archive_path
213
+ atomizer << archive_commitfile_path
214
+
215
+ if owner || group
216
+ create_arhive_with_owner_substitution!
217
+ else
218
+ create_simple_archive!
219
+ end
220
+
221
+ File.write archive_commitfile_path, repo_latest_commit
222
+ end
223
+
224
+ def archive_exists?
225
+ File.exist? archive_commitfile_path
226
+ end
227
+
228
+ def add_archive
229
+ builder.docker.add_artifact archive_path, archive_filename, where_to_add, step: :prepare
230
+ end
231
+
232
+ def sudo_format_user(user)
233
+ user.to_i.to_s == user ? "\\\##{user}" : user
234
+ end
235
+
236
+ def sudo
237
+ sudo = ''
238
+
239
+ if owner || group
240
+ sudo = 'sudo '
241
+ sudo += "-u #{sudo_format_user(owner)} " if owner
242
+ sudo += "-g #{sudo_format_user(group)} " if group
243
+ end
244
+
245
+ sudo
246
+ end
247
+
248
+ def add_patch(filename, step:)
249
+ builder.docker.add_artifact(build_path(filename), filename, '/tmp', step: step)
250
+
251
+ builder.docker.run [
252
+ "zcat /tmp/#{filename} | #{sudo}git apply --whitespace=nowarn --directory=#{where_to_add}",
253
+ "rm /tmp/#{filename}"
254
+ ], step: step
255
+ end
256
+
257
+ def create_patch!(from, filename, commitfile_path)
258
+ atomizer << build_path(filename)
259
+ atomizer << commitfile_path
260
+
261
+ repo.git_bare "diff --binary #{from}..#{repo_latest_commit}#{" --relative=#{cwd}" if cwd} #{paths(true)} | gzip > #{build_path filename}"
262
+ File.write commitfile_path, repo_latest_commit
263
+ end
264
+
265
+ def layer_filename(layer, ending)
266
+ filename "_layer_#{layer.is_a?(Fixnum) ? format('%04d', layer) : layer}#{ending}"
267
+ end
268
+
269
+ def layer_patch_filename(layer)
270
+ layer_filename(layer, '.patch.gz')
271
+ end
272
+
273
+ def layer_patch_path(layer)
274
+ build_path layer_patch_filename(layer)
275
+ end
276
+
277
+ def layer_commitfile_path(layer)
278
+ build_path layer_filename(layer, '.commit')
279
+ end
280
+
281
+ def layer_commit(layer)
282
+ File.read layer_commitfile_path(layer) if File.exist? layer_commitfile_path(layer)
283
+ end
284
+
285
+ def layers
286
+ Dir.glob(layer_commitfile_path('*')).map { |path| Integer(path.gsub(/.*_(\d+)\.commit$/, '\\1')) }.sort
287
+ end
288
+
289
+ def create_layer_patch!(from, layer)
290
+ create_patch! from, layer_patch_filename(layer), layer_commitfile_path(layer)
291
+ end
292
+
293
+ def add_layer_patch(layer)
294
+ add_patch layer_patch_filename(layer), step: :build
295
+ end
296
+
297
+ def latest_patch_filename
298
+ filename '_latest.patch.gz'
299
+ end
300
+
301
+ def latest_patch_path
302
+ build_path latest_patch_filename
303
+ end
304
+
305
+ def latest_commitfile_path
306
+ build_path filename '_latest.commit'
307
+ end
308
+
309
+ def latest_commit
310
+ File.read latest_commitfile_path if File.exist? latest_commitfile_path
311
+ end
312
+
313
+ def create_latest_patch!(from)
314
+ create_patch! from, latest_patch_filename, latest_commitfile_path
315
+ end
316
+
317
+ def add_latest_patch
318
+ add_patch latest_patch_filename, step: :setup
319
+ end
320
+
321
+ def remove_latest!
322
+ FileUtils.rm_f [latest_patch_path, latest_commitfile_path]
323
+ end
324
+
325
+ def lock(**kwargs, &block)
326
+ builder.filelock(build_path(filename('.lock')), error_message: "Branch #{branch} of artifact #{name ? " #{name}" : nil} #{repo.name}" \
327
+ " (#{repo.dir_path}) in use! Try again later.", **kwargs, &block)
328
+ end
329
+ end
330
+ end