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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YTQzOThlMTg4MDA3YzQ0MjE4ODgyZjgyMzQ4M2RiMTgzMmRlN2ZjZg==
5
+ data.tar.gz: !binary |-
6
+ ODQ2ODVjOTY3OTE0M2I1NzQ4MjU1ZDczODE3MTFmNDJmYTI4NDQ2OQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YzU1NWRjMDAyOWNjMGRiN2RkZGQyMmFhYzhlODQwYTc1ZGJiMzI0MDJkODkw
10
+ Zjk5YTA2YTg1MDlmN2M0N2E5MWM1YmVhNmVmZTQ1MTlhMDRkY2IxOGI5OGQ1
11
+ NTRkNzU0OTU4ZDM3ZDVlNjE3Y2U1N2VhODk3ODA0NThiYTE2Yzc=
12
+ data.tar.gz: !binary |-
13
+ ZGQ1ZTQ2ZGJmMmEwMWZjYWQ3MDYxMjg5MGNiNGQyNDc5NmQ0YWFmMWU3NmUx
14
+ MjkyMjllNjczNDAzMzZkYzNiY2E3YTk5MjVkODMyY2Q2NDZlMTVlMDIxZTI3
15
+ Y2U0YzBmNDIxOGQwYzE2N2EyMTY5MjdmZTFlYTI0NjUyZTgxMjU=
data/bin/dapp ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- mode: ruby -*-
3
+ # vi: set ft=ruby :
4
+
5
+ require 'rubygems'
6
+ require 'dapp'
7
+
8
+ Dapp::CLI.new.run
data/lib/dapp.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+ require 'tmpdir'
4
+ require 'digest'
5
+ require 'timeout'
6
+ require 'base64'
7
+ require 'mixlib/shellout'
8
+
9
+ require 'dapp/version'
10
+ require 'dapp/cli'
11
+ require 'dapp/builder/chefify'
12
+ require 'dapp/builder/centos7'
13
+ require 'dapp/builder/cascade_tagging'
14
+ require 'dapp/filelock'
15
+ require 'dapp/builder'
16
+ require 'dapp/docker'
17
+ require 'dapp/atomizer'
18
+ require 'dapp/git_repo/base'
19
+ require 'dapp/git_repo/chronicler'
20
+ require 'dapp/git_repo/remote'
21
+ require 'dapp/git_artifact'
@@ -0,0 +1,53 @@
1
+ module Dapp
2
+ # "Transaction" journal with rollback (mainly to protect cache fill with unbuildable configuration)
3
+ class Atomizer
4
+ def initialize(builder, file_path)
5
+ @builder = builder
6
+ @file_path = file_path
7
+ @file = open
8
+
9
+ builder.register_atomizer self
10
+ end
11
+
12
+ def <<(path)
13
+ file.puts path
14
+ end
15
+
16
+ def commit!
17
+ @file.truncate(0)
18
+ @file.close
19
+ @file = open
20
+ end
21
+
22
+ protected
23
+
24
+ attr_reader :file_path
25
+ attr_reader :builder
26
+
27
+ attr_reader :file
28
+
29
+ def open
30
+ file = File.open(file_path, File::RDWR | File::CREAT, 0644)
31
+
32
+ file.sync = true
33
+
34
+ Timeout.timeout(10) do
35
+ file.flock(File::LOCK_EX)
36
+ end
37
+
38
+ if (not_commited_paths = file.read.lines.map(&:strip))
39
+ FileUtils.rm_rf not_commited_paths
40
+ end
41
+
42
+ file.truncate(0)
43
+ file.rewind
44
+
45
+ file
46
+ rescue Timeout::Error
47
+ file.close
48
+
49
+ STDERR.puts 'Atomizer already in use! Try again later.'
50
+ exit 1
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,217 @@
1
+ module Dapp
2
+ # Main class that does all stuff
3
+ class Builder
4
+ include Chefify
5
+ include Centos7
6
+ include CascadeTagging
7
+ include Filelock
8
+
9
+ class << self
10
+ def default_opts
11
+ @default_opts ||= {}
12
+ end
13
+
14
+ def dappfiles_paths(path, pattern = '*')
15
+ pattern.split('-').instance_eval { count.downto(1).map { |n| slice(0, n).join('-') } }
16
+ .map { |p| Dir.glob(File.join([path, p, default_opts[:dappfile_name] || 'Dappfile'].compact)) }.find(&:any?) || []
17
+ end
18
+
19
+ def process_directory(path, pattern = '*')
20
+ dappfiles_paths(path, pattern).map { |dappfile_path| process_file(dappfile_path, app_filter: pattern).builded_apps }.flatten
21
+ end
22
+
23
+ def process_file(dappfile_path, app_filter: '*')
24
+ new(dappfile_path: dappfile_path, app_filter: app_filter) do |builder|
25
+ builder.log "Processing application #{builder.name} (#{dappfile_path})"
26
+
27
+ # indent all subsequent messages
28
+ builder.indent_log
29
+
30
+ # eval instructions from file
31
+ builder.instance_eval File.read(dappfile_path), dappfile_path
32
+
33
+ # commit atomizers
34
+ builder.commit_atomizers!
35
+ end
36
+ end
37
+ end
38
+
39
+ def log(message)
40
+ puts ' ' * opts[:log_indent] + ' * ' + message if opts[:log_verbose] || !opts[:log_quiet]
41
+ end
42
+
43
+ def shellout(*args, log_verbose: false, **kwargs)
44
+ kwargs[:live_stream] = STDOUT if log_verbose && opts[:log_verbose]
45
+ Mixlib::ShellOut.new(*args, :timeout => 3600, **kwargs).run_command.tap(&:error!)
46
+ end
47
+
48
+ def home_path(*path)
49
+ path.compact.inject(Pathname.new(opts[:home_path]), &:+).expand_path.to_s
50
+ end
51
+
52
+ def build_path(*path)
53
+ path.compact.inject(Pathname.new(opts[:build_path]), &:+).expand_path.tap do |p|
54
+ FileUtils.mkdir_p p.parent
55
+ end.to_s
56
+ end
57
+
58
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
59
+ def initialize(**options)
60
+ opts.merge! self.class.default_opts
61
+ opts.merge! options
62
+
63
+ # default log indentation
64
+ opts[:log_indent] = 0
65
+
66
+ # basename
67
+ if opts[:name]
68
+ opts[:basename] = opts[:name]
69
+ opts[:name] = nil
70
+ elsif opts[:dappfile_path]
71
+ opts[:basename] ||= Pathname.new(opts[:dappfile_path]).expand_path.parent.basename
72
+ end
73
+
74
+ # home path
75
+ opts[:home_path] ||= Pathname.new(opts[:dappfile_path] || 'fakedir').parent.expand_path.to_s
76
+
77
+ # build path
78
+ opts[:build_path] = opts[:build_dir] ? opts[:build_dir] : home_path('build')
79
+ opts[:build_path] = build_path opts[:basename] if opts[:shared_build_dir]
80
+
81
+ # home branch
82
+ @home_branch = shellout("git -C #{home_path} rev-parse --abbrev-ref HEAD").stdout.strip
83
+
84
+ # atomizers
85
+ @atomizers = []
86
+
87
+ # account builded apps
88
+ @builded_apps = []
89
+
90
+ lock do
91
+ yield self
92
+ end
93
+ end
94
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
95
+
96
+ def indent_log
97
+ opts[:log_indent] += 1
98
+ end
99
+
100
+ attr_reader :home_branch
101
+
102
+ def builded_apps
103
+ @builded_apps.dup
104
+ end
105
+
106
+ def docker
107
+ docker_stack.last
108
+ end
109
+
110
+ def scope(&blk)
111
+ stack_settings(&blk)
112
+ end
113
+
114
+ def app(name)
115
+ log "Processing #{self.name}-#{name}"
116
+
117
+ name = "#{opts[:name]}-#{name}" if opts[:name]
118
+
119
+ stack_settings name: name, log_indent: opts[:log_indent] + 1 do
120
+ docker.name name
121
+
122
+ yield
123
+ end
124
+ end
125
+
126
+ def name
127
+ [opts[:basename], opts[:name]].compact.join '-'
128
+ end
129
+
130
+ def add_artifact_from_git(url, where_to_add, branch: opts[:git_artifact_branch], ssh_key_path: nil, **kwargs)
131
+ log "Adding artifact from git (#{url} to #{where_to_add}, branch: #{branch})"
132
+
133
+ # extract git repo name from url
134
+ repo_name = url.gsub(%r{.*?([^\/ ]+)\.git}, '\\1')
135
+
136
+ # clone or fetch
137
+ repo = GitRepo::Remote.new(self, repo_name, url: url, ssh_key_path: ssh_key_path)
138
+ repo.fetch!(branch)
139
+
140
+ # add artifact
141
+ artifact = GitArtifact.new(self, repo, where_to_add, flush_cache: opts[:flush_cache], branch: branch, **kwargs)
142
+ artifact.add_multilayer!
143
+ end
144
+
145
+ def build(**_kwargs)
146
+ # check app name
147
+ unless !opts[:app_filter] || File.fnmatch("#{opts[:app_filter]}*", name)
148
+ log "Skipped (does not match filter: #{opts[:app_filter]})!"
149
+ return false
150
+ end
151
+
152
+ # build image
153
+ log 'Building'
154
+ image_id = docker.build
155
+
156
+ # apply cascade tagging
157
+ tag_cascade image_id
158
+
159
+ # push to registry
160
+ if opts[:docker_registry]
161
+ log 'Pushing to registry'
162
+ docker.push name: name, registry: opts[:docker_registry]
163
+ end
164
+
165
+ # count it
166
+ @builded_apps << name
167
+
168
+ image_id
169
+ end
170
+
171
+ def tag(image_id, name: nil, tag: nil, registry: nil)
172
+ return unless name && tag
173
+
174
+ new = { name: name, tag: tag, registry: registry }
175
+ docker.tag image_id, new
176
+ end
177
+
178
+ def register_atomizer(atomizer)
179
+ atomizers << atomizer
180
+ end
181
+
182
+ def commit_atomizers!
183
+ atomizers.each(&:commit!)
184
+ end
185
+
186
+ protected
187
+
188
+ attr_reader :atomizers
189
+
190
+ def opts
191
+ opts_stack.last
192
+ end
193
+
194
+ def opts_stack
195
+ @opts_stack ||= [{}]
196
+ end
197
+
198
+ def docker_stack
199
+ @docker_stack ||= [Docker.new(self)]
200
+ end
201
+
202
+ def stack_settings(**options)
203
+ opts_stack.push opts.merge(options).dup
204
+ docker_stack.push docker.dup
205
+
206
+ yield
207
+ ensure
208
+ docker_stack.pop
209
+ opts_stack.pop
210
+ end
211
+
212
+ def lock(**kwargs, &block)
213
+ filelock(build_path("#{home_branch}.lock"), error_message: "Application #{opts[:basename]} (#{home_branch}) in use! Try again later.", **kwargs,
214
+ &block)
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,48 @@
1
+ module Dapp
2
+ class Builder
3
+ # Cascade tagging strategy
4
+ module CascadeTagging
5
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
6
+ def tag_cascade(image_id)
7
+ return unless opts[:cascade_tagging]
8
+
9
+ log 'Applying cascade tagging'
10
+
11
+ opts[:build_history_length] ||= 10
12
+
13
+ i = {
14
+ name: name,
15
+ tag: home_branch,
16
+ registry: opts[:docker_registry]
17
+ }
18
+
19
+ # return if nothing changed
20
+ return if image_id == docker.image_id(i)
21
+
22
+ # remove excess tags
23
+ tags_to_remove = docker.images(name: i[:name], registry: i[:registry])
24
+ .map { |image| image[:tag] }
25
+ .select { |tag| tag.start_with?("#{i[:tag]}_") && tag.sub(/^#{i[:tag]}_/, '').to_i >= opts[:build_history_length] }
26
+ tags_to_remove.each do |tag_to_remove|
27
+ docker.rmi i.merge(tag: tag_to_remove)
28
+ end
29
+
30
+ # shift old images: 1 -> 2, 2 -> 3, ..., n -> n+1
31
+ (opts[:build_history_length] - 1).downto(1).each do |n|
32
+ origin = i.merge(tag: "#{i[:tag]}_#{n}")
33
+
34
+ if docker.image_exists?(**origin)
35
+ docker.tag origin, i.merge(tag: "#{i[:tag]}_#{n + 1}")
36
+ end
37
+ end
38
+
39
+ # shift top -> 1
40
+ docker.tag i, i.merge(tag: "#{i[:tag]}_1") if docker.image_exists?(**i)
41
+
42
+ # tag top
43
+ docker.tag image_id, i
44
+ end
45
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ module Dapp
2
+ class Builder
3
+ # Centos7 support
4
+ module Centos7
5
+ # rubocop:disable Metrics/MethodLength:
6
+ def from_centos7
7
+ # use centos7
8
+ docker.from 'centos:7'
9
+
10
+ # add real systemd
11
+ docker.env container: 'docker', step: :begining
12
+ docker.run 'yum -y swap -- remove systemd-container systemd-container-libs -- install systemd systemd-libs', step: :begining
13
+ docker.run(
14
+ 'yum -y update; yum clean all',
15
+ '(cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done)',
16
+ 'rm -f /lib/systemd/system/multi-user.target.wants/*',
17
+ 'rm -f /etc/systemd/system/*.wants/*',
18
+ 'rm -f /lib/systemd/system/local-fs.target.wants/*',
19
+ 'rm -f /lib/systemd/system/sockets.target.wants/*udev*',
20
+ 'rm -f /lib/systemd/system/sockets.target.wants/*initctl*',
21
+ 'rm -f /lib/systemd/system/basic.target.wants/*',
22
+ 'rm -f /lib/systemd/system/anaconda.target.wants/*',
23
+ step: :begining
24
+ )
25
+ docker.volume '/sys/fs/cgroup', step: :begining
26
+ docker.cmd '/usr/sbin/init', step: :begining
27
+
28
+ # cache yum fastestmirror
29
+ docker.run 'yum makecache', step: :begining
30
+
31
+ # TERM & utf8
32
+ docker.run 'localedef -c -f UTF-8 -i en_US en_US.UTF-8', step: :begining
33
+ docker.env TERM: 'xterm', LANG: 'en_US.UTF-8', LANGUAGE: 'en_US:en', LC_ALL: 'en_US.UTF-8', step: :begining
34
+
35
+ # centos hacks
36
+ docker.run(
37
+ 'sed \'s/\(-\?session\s\+optional\s\+pam_systemd\.so.*\)/#\1/g\' -i /etc/pam.d/system-auth',
38
+ 'yum install -y sudo git',
39
+ 'echo \'Defaults:root !requiretty\' >> /etc/sudoers',
40
+ step: :begining
41
+ )
42
+ end
43
+ # rubocop:enable Metrics/MethodLength
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,101 @@
1
+ module Dapp
2
+ class Builder
3
+ # Build using chef "dapp" cookbooks
4
+ module Chefify
5
+ def dappit(*extra_dapps, chef_version: '12.4.3', **_kwargs)
6
+ log 'Adding dapp chef cookbook artifact and chef solo run'
7
+
8
+ setup_dapp_chef chef_version
9
+
10
+ # run chef solo
11
+ [:prepare, :build, :setup].each do |step|
12
+ # run chef-solo for extra dapps
13
+ extra_dapps.each do |extra_dapp|
14
+ if dapp_chef_cookbooks_artifact.exists_in_step? "cookbooks/#{extra_dapp}/recipes/#{step}.rb", step
15
+ # FIXME: env ???
16
+ docker.run "chef-solo -c /usr/share/dapp/chef_solo.rb -o #{extra_dapp}::#{step},env-#{opts[:basename]}::void", step: step
17
+ end
18
+ end
19
+
20
+ # run chef-solo for app
21
+ recipe = [opts[:name], step].compact.join '-'
22
+ # FIXME: env ???
23
+ if dapp_chef_cookbooks_artifact.exists_in_step? "cookbooks/env-#{opts[:basename]}/recipes/#{recipe}.rb", step
24
+ docker.run "chef-solo -c /usr/share/dapp/chef_solo.rb -o env-#{opts[:basename]}::#{recipe}", step: step
25
+ end
26
+ end
27
+ end
28
+
29
+ def build_dapp(*args, extra_dapps: [], **kwargs, &blk)
30
+ stack_settings do
31
+ dappit(*extra_dapps, **kwargs)
32
+
33
+ build(*args, **kwargs, &blk)
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def dapp_chef_cookbooks_artifact
40
+ unless @dapp_chef_cookbooks_artifact
41
+ # init cronicler
42
+ repo = GitRepo::Chronicler.new(self, 'dapp_cookbooks', build_path: home_branch)
43
+
44
+ # vendor cookbooks
45
+ shellout "berks vendor --berksfile=#{home_path 'Berksfile'} #{repo.chronodir_path 'cookbooks'}", log_verbose: true
46
+
47
+ # create void receipt
48
+ # FIXME: env ???
49
+ FileUtils.touch repo.chronodir_path 'cookbooks', "env-#{opts[:basename]}", 'recipes', 'void.rb'
50
+
51
+ # commit (if smth changed)
52
+ repo.commit!
53
+
54
+ # init artifact
55
+ @dapp_chef_cookbooks_artifact = GitArtifact.new(self, repo, '/usr/share/dapp/chef_repo/cookbooks',
56
+ cwd: 'cookbooks', build_path: home_branch, flush_cache: opts[:flush_cache])
57
+ end
58
+
59
+ @dapp_chef_cookbooks_artifact
60
+ end
61
+
62
+ def install_chef_and_setup_chef_solo(chef_version)
63
+ docker.run(
64
+ "curl -L https://www.opscode.com/chef/install.sh | bash -s -- -v #{chef_version}",
65
+ 'mkdir -p /usr/share/dapp/chef_repo /var/cache/dapp/chef',
66
+ 'echo file_cache_path \\"/var/cache/dapp/chef\\" > /usr/share/dapp/chef_solo.rb',
67
+ 'echo cookbook_path \\"/usr/share/dapp/chef_repo/cookbooks\\" >> /usr/share/dapp/chef_solo.rb',
68
+ step: :begining
69
+ )
70
+ end
71
+
72
+ def run_chef_solo_for_dapp_common
73
+ [:prepare, :build, :setup].each do |step|
74
+ if dapp_chef_cookbooks_artifact.exists_in_step? "cookbooks/dapp-common/recipes/#{step}.rb", step
75
+ # FIXME: env ???
76
+ docker.run "chef-solo -c /usr/share/dapp/chef_solo.rb -o dapp-common::#{step},env-#{opts[:basename]}::void", step: step
77
+ end
78
+ end
79
+ end
80
+
81
+ def setup_dapp_chef(chef_version)
82
+ if opts[:dapp_chef_version]
83
+ raise "dapp chef version mismatch, version #{opts[:dapp_chef_version]} already installed" if opts[:dapp_chef_version] != chef_version
84
+ return
85
+ end
86
+
87
+ # install chef, setup chef_solo
88
+ install_chef_and_setup_chef_solo(chef_version)
89
+
90
+ # add cookbooks
91
+ dapp_chef_cookbooks_artifact.add_multilayer!
92
+
93
+ # mark chef as installed
94
+ opts[:dapp_chef_version] = chef_version
95
+
96
+ # run chef solo for dapp-common
97
+ run_chef_solo_for_dapp_common
98
+ end
99
+ end
100
+ end
101
+ end