dapp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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