dapp 0.31.28 → 0.32.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/dapp +1 -23
- data/lib/dapp/dapp/dappfile.rb +2 -2
- data/lib/dapp/dapp/sentry.rb +0 -1
- data/lib/dapp/dimg/builder/ansible.rb +3 -1
- data/lib/dapp/dimg/builder/base.rb +2 -2
- data/lib/dapp/dimg/config/directive/shell/dimg.rb +1 -1
- data/lib/dapp/dimg/dapp/command/cleanup_repo.rb +40 -26
- data/lib/dapp/dimg/dapp/dappfile.rb +1 -1
- data/lib/dapp/dimg/dimg.rb +1 -0
- data/lib/dapp/dimg/docker_registry/base/authorization.rb +1 -16
- data/lib/dapp/helper/sha256.rb +1 -1
- data/lib/dapp/kube/kubernetes/client.rb +0 -6
- data/lib/dapp/kube/kubernetes/manager/deployment.rb +18 -30
- data/lib/dapp/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7836539f8a917786c4db7778853720687f41b3dc80eaaddee4b7e9cf13a7e8f5
|
4
|
+
data.tar.gz: 84eec391c97ddebb98732b6dc8729f606a7cbbb2c5eaf1c4b398e4a22de30736
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e44e256557b647a045f379df38c06f88ad2911a49e691112e3ce3d970ccc4afcf8f09d2f0381478924f41560d50c58bd8fb66ce97951a4a68ae280548ef70e70
|
7
|
+
data.tar.gz: 37d984525171b58ba24a6ea18a208b87f4e50de784ed8098958c9df0c2eb9893cc3b12ec95b4f2d17ebde90c7785299dd563f1c9168dbc0f8e02aefda1d3b108
|
data/bin/dapp
CHANGED
@@ -74,29 +74,7 @@ set_gitlab_cancel_handler if ENV["GITLAB_CI"]
|
|
74
74
|
begin
|
75
75
|
begin
|
76
76
|
begin
|
77
|
-
|
78
|
-
Dapp::CLI.new.run
|
79
|
-
ensure
|
80
|
-
if Time.now.to_date < Date.parse("2019-11-01")
|
81
|
-
STDERR.puts
|
82
|
-
STDERR.puts ::Paint["###################################################################", :red, :bold]
|
83
|
-
STDERR.puts ::Paint["### DEPRECATION WARNING! ###", :red, :bold]
|
84
|
-
STDERR.puts ::Paint["### Dapp will be deprecated for use starting with 01.11.2019! ###", :red, :bold]
|
85
|
-
STDERR.puts ::Paint["### Please port your project to werf: ###", :red, :bold]
|
86
|
-
STDERR.puts ::Paint["### https://werf.io ###", :red, :bold]
|
87
|
-
STDERR.puts ::Paint["###################################################################", :red, :bold]
|
88
|
-
STDERR.puts
|
89
|
-
else
|
90
|
-
STDERR.puts
|
91
|
-
STDERR.puts ::Paint["######################################################################", :red, :bold]
|
92
|
-
STDERR.puts ::Paint["### DEPRECATION WARNING! ###", :red, :bold]
|
93
|
-
STDERR.puts ::Paint["### Dapp is deprecated for use and will not receive any support! ###", :red, :bold]
|
94
|
-
STDERR.puts ::Paint["### Please port your project to werf: ###", :red, :bold]
|
95
|
-
STDERR.puts ::Paint["### https://werf.io ###", :red, :bold]
|
96
|
-
STDERR.puts ::Paint["######################################################################", :red, :bold]
|
97
|
-
STDERR.puts
|
98
|
-
end
|
99
|
-
end
|
77
|
+
Dapp::CLI.new.run
|
100
78
|
rescue Dapp::Error::Base => e
|
101
79
|
unless (message = Dapp::Helper::NetStatus.before_error_message(e)).empty?
|
102
80
|
$stderr.puts(message)
|
data/lib/dapp/dapp/dappfile.rb
CHANGED
@@ -126,12 +126,12 @@ module Dapp
|
|
126
126
|
return if File.exists? dappfile_yml_bin_path
|
127
127
|
|
128
128
|
log_process("Downloading dappfile-yml dapp dependency") do
|
129
|
-
location = URI("https://dl.bintray.com/dapp
|
129
|
+
location = URI("https://dl.bintray.com/flant/dapp/#{::Dapp::VERSION}/dappfile-yml")
|
130
130
|
|
131
131
|
tmp_bin_path = File.join(self.class.tmp_base_dir, "dappfile-yml-#{SecureRandom.uuid}")
|
132
132
|
::Dapp::Downloader.download(location, tmp_bin_path, show_progress: true, progress_titile: dappfile_yml_bin_path)
|
133
133
|
|
134
|
-
checksum_location = URI("https://dl.bintray.com/dapp
|
134
|
+
checksum_location = URI("https://dl.bintray.com/flant/dapp/#{::Dapp::VERSION}/dappfile-yml.sha")
|
135
135
|
tmp_bin_checksum_path = tmp_bin_path + ".checksum"
|
136
136
|
::Dapp::Downloader.download(checksum_location, tmp_bin_checksum_path)
|
137
137
|
|
data/lib/dapp/dapp/sentry.rb
CHANGED
@@ -50,7 +50,6 @@ module Dapp
|
|
50
50
|
"build-dir" => self.build_dir,
|
51
51
|
"options" => self.options,
|
52
52
|
"env-options" => {
|
53
|
-
"DAPP_FORCE_SAVE_CACHE" => ENV["DAPP_FORCE_SAVE_CACHE"],
|
54
53
|
"DAPP_BIN_DAPPFILE_YML" => ENV["DAPP_BIN_DAPPFILE_YML"],
|
55
54
|
"ANSIBLE_ARGS" => ENV["ANSIBLE_ARGS"],
|
56
55
|
"DAPP_CHEF_DEBUG" => ENV["DAPP_CHEF_DEBUG"],
|
@@ -138,7 +138,9 @@ module Dapp
|
|
138
138
|
|
139
139
|
define_method("#{stage}_checksum") do
|
140
140
|
checksum_args = []
|
141
|
-
|
141
|
+
(dimg.config._ansible[stage.to_s] || []).each do |task|
|
142
|
+
checksum_args << JSON.dump(task["config"])
|
143
|
+
end
|
142
144
|
checksum_args << public_send("#{stage}_version_checksum")
|
143
145
|
_checksum checksum_args
|
144
146
|
end
|
@@ -74,8 +74,8 @@ module Dapp
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def _checksum(*args)
|
77
|
-
return if args.flatten.compact.delete_if { |val| val.respond_to?(:empty?) && val.empty? }.empty?
|
78
|
-
dimg.hashsum
|
77
|
+
return if (format_args = args.flatten.compact.delete_if { |val| val.respond_to?(:empty?) && val.empty? }).empty?
|
78
|
+
dimg.hashsum format_args
|
79
79
|
end
|
80
80
|
end # Builder::Base
|
81
81
|
end # Dimg
|
@@ -25,7 +25,7 @@ module Dapp
|
|
25
25
|
|
26
26
|
define_method "_#{stage}_version" do
|
27
27
|
return [] if (variable = instance_variable_get("@_#{stage}")).nil?
|
28
|
-
variable._version
|
28
|
+
variable._version
|
29
29
|
end
|
30
30
|
end
|
31
31
|
[:before_install, :before_setup, :install, :setup].each(&method(:stage_command_generator))
|
@@ -3,8 +3,8 @@ module Dapp
|
|
3
3
|
module Dapp
|
4
4
|
module Command
|
5
5
|
module CleanupRepo
|
6
|
-
DATE_POLICY = Time.now.to_i - 60 * 60 * 24 * 30
|
7
6
|
GIT_TAGS_LIMIT_POLICY = 10
|
7
|
+
EXPIRY_DATE_PERIOD_POLICY = 60 * 60 * 24 * 30
|
8
8
|
|
9
9
|
def cleanup_repo
|
10
10
|
lock_repo(repo = option_repo) do
|
@@ -87,36 +87,56 @@ module Dapp
|
|
87
87
|
%w(git_tag git_commit).each_with_object([]) do |scheme, dimgs_images|
|
88
88
|
dimgs_images.concat begin
|
89
89
|
detailed_dimgs_images_by_scheme[scheme].select do |dimg|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
git_own_repo.commit_exists?(dimg[:tag])
|
95
|
-
end
|
90
|
+
if scheme == 'git_tag'
|
91
|
+
consistent_git_tags.include?(dimg[:tag])
|
92
|
+
elsif scheme == 'git_commit'
|
93
|
+
git_own_repo.commit_exists?(dimg[:tag])
|
96
94
|
end
|
97
95
|
end
|
98
96
|
end
|
99
97
|
end.tap do |detailed_dimgs_images|
|
100
98
|
sorted_detailed_dimgs_images = detailed_dimgs_images.sort_by { |dimg| dimg[:created_at] }.reverse
|
101
99
|
expired_dimgs_images, not_expired_dimgs_images = sorted_detailed_dimgs_images.partition do |dimg_image|
|
102
|
-
dimg_image[:created_at] <
|
100
|
+
dimg_image[:created_at] < expiry_date_policy
|
103
101
|
end
|
104
102
|
|
105
|
-
log_step_with_indent(:"date policy (before #{DateTime.strptime(
|
103
|
+
log_step_with_indent(:"date policy (before #{DateTime.strptime(expiry_date_policy.to_s, '%s')})") do
|
106
104
|
expired_dimgs_images.each { |dimg| delete_repo_image(registry, dimg) }
|
107
105
|
end
|
108
106
|
|
109
107
|
{}.tap do |images_by_dimg|
|
110
108
|
not_expired_dimgs_images.each { |dimg| (images_by_dimg[dimg[:dimg]] ||= []) << dimg }
|
111
109
|
images_by_dimg.each do |dimg, images|
|
112
|
-
log_step_with_indent(:"limit policy (> #{
|
113
|
-
images[
|
114
|
-
end unless images[
|
110
|
+
log_step_with_indent(:"limit policy (> #{git_tag_limit_policy}) (`#{dimg || "nameless"}` dimg)") do
|
111
|
+
images[git_tag_limit_policy..-1].each { |dimg| delete_repo_image(registry, dimg) }
|
112
|
+
end unless images[git_tag_limit_policy..-1].nil?
|
115
113
|
end
|
116
114
|
end
|
117
115
|
end
|
118
116
|
end
|
119
117
|
|
118
|
+
def expiry_date_policy
|
119
|
+
@expiry_date_policy = begin
|
120
|
+
expiry_date_period_policy = policy_value('EXPIRY_DATE_PERIOD_POLICY', default: EXPIRY_DATE_PERIOD_POLICY)
|
121
|
+
Time.now.to_i - expiry_date_period_policy
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def git_tag_limit_policy
|
126
|
+
@git_tag_limit_policy ||= policy_value('GIT_TAGS_LIMIT_POLICY', default: GIT_TAGS_LIMIT_POLICY)
|
127
|
+
end
|
128
|
+
|
129
|
+
def policy_value(env_key, default:)
|
130
|
+
return default if (val = ENV[env_key]).nil?
|
131
|
+
|
132
|
+
if val.to_i.to_s == val
|
133
|
+
val.to_i
|
134
|
+
else
|
135
|
+
log_warning("WARNING: `#{env_key}` value `#{val}` is ignored (using default value `#{default}`)!")
|
136
|
+
default
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
120
140
|
def git_tag_by_consistent_git_tag(consistent_git_tag)
|
121
141
|
git_tag_by_consistent_tag_name[consistent_git_tag]
|
122
142
|
end
|
@@ -180,65 +200,59 @@ module Dapp
|
|
180
200
|
# pod items[] spec containers[] image
|
181
201
|
def pod_images(client)
|
182
202
|
client.pod_list['items'].map do |item|
|
183
|
-
|
203
|
+
item['spec']['containers'].map{ |cont| cont['image'] }
|
184
204
|
end
|
185
205
|
end
|
186
206
|
|
187
207
|
# cronjob items[] spec jobTemplate spec template spec containers[] image
|
188
208
|
def cronjob_images(client)
|
189
209
|
client.cronjob_list['items'].map do |item|
|
190
|
-
|
210
|
+
item['spec']['jobTemplate']['spec']['template']['spec']['containers'].map{ |cont| cont['image'] }
|
191
211
|
end
|
192
212
|
end
|
193
213
|
|
194
214
|
# daemonsets items[] spec template spec containers[] image
|
195
215
|
def daemonset_images(client)
|
196
216
|
client.daemonset_list['items'].map do |item|
|
197
|
-
|
217
|
+
item['spec']['template']['spec']['containers'].map{ |cont| cont['image'] }
|
198
218
|
end
|
199
219
|
end
|
200
220
|
|
201
221
|
# deployment items[] spec template spec containers[] image
|
202
222
|
def deployment_images(client)
|
203
223
|
client.deployment_list['items'].map do |item|
|
204
|
-
|
224
|
+
item['spec']['template']['spec']['containers'].map{ |cont| cont['image'] }
|
205
225
|
end
|
206
226
|
end
|
207
227
|
|
208
228
|
# job items[] spec template spec containers[] image
|
209
229
|
def job_images(client)
|
210
230
|
client.job_list['items'].map do |item|
|
211
|
-
|
231
|
+
item['spec']['template']['spec']['containers'].map{ |cont| cont['image'] }
|
212
232
|
end
|
213
233
|
end
|
214
234
|
|
215
235
|
# replicasets items[] spec template spec containers[] image
|
216
236
|
def replicaset_images(client)
|
217
237
|
client.replicaset_list['items'].map do |item|
|
218
|
-
|
238
|
+
item['spec']['template']['spec']['containers'].map{ |cont| cont['image'] }
|
219
239
|
end
|
220
240
|
end
|
221
241
|
|
222
242
|
# replicasets items[] spec template spec containers[] image
|
223
243
|
def statefulset_images(client)
|
224
244
|
client.statefulset_list['items'].map do |item|
|
225
|
-
|
245
|
+
item['spec']['template']['spec']['containers'].map{ |cont| cont['image'] }
|
226
246
|
end
|
227
247
|
end
|
228
248
|
|
229
249
|
# replicationcontroller items[] spec template spec containers[] image
|
230
250
|
def replicationcontroller_images(client)
|
231
251
|
client.replicationcontroller_list['items'].map do |item|
|
232
|
-
|
252
|
+
item['spec']['template']['spec']['containers'].map{ |cont| cont['image'] }
|
233
253
|
end
|
234
254
|
end
|
235
255
|
|
236
|
-
def images_from_pod_spec(pod_spec)
|
237
|
-
containers = Array(pod_spec['spec']['containers'])
|
238
|
-
initContainers = Array(pod_spec['spec']['initContainers'])
|
239
|
-
(containers + initContainers).map { |cont| cont['image'] }
|
240
|
-
end
|
241
|
-
|
242
256
|
def without_kube?
|
243
257
|
!!options[:without_kube]
|
244
258
|
end
|
data/lib/dapp/dimg/dimg.rb
CHANGED
@@ -145,6 +145,7 @@ module Dapp
|
|
145
145
|
|
146
146
|
def build_export_image!(image_name, scheme_name:)
|
147
147
|
Image::Dimg.image_by_name(name: image_name, from: last_stage.image, dapp: dapp).tap do |export_image|
|
148
|
+
export_image.untag! if export_image.built?
|
148
149
|
export_image.add_service_change_label(:'dapp-tag-scheme' => scheme_name)
|
149
150
|
export_image.add_service_change_label(:'dapp-dimg' => true)
|
150
151
|
export_image.build!
|
@@ -29,25 +29,10 @@ module Dapp
|
|
29
29
|
[:realm, :service, :scope].map do |option|
|
30
30
|
/#{option}="([[^"].]*)/ =~ header
|
31
31
|
next unless Regexp.last_match(1)
|
32
|
-
|
33
|
-
option_value = begin
|
34
|
-
if option == :scope
|
35
|
-
handle_scope_option(Regexp.last_match(1))
|
36
|
-
else
|
37
|
-
Regexp.last_match(1)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
[option, option_value]
|
32
|
+
[option, Regexp.last_match(1)]
|
42
33
|
end.compact.to_h
|
43
34
|
end
|
44
35
|
|
45
|
-
def handle_scope_option(resourcescope)
|
46
|
-
resource_type, resource_name, actions = resourcescope.split(":")
|
47
|
-
actions = actions.split(",").map { |action| action == "delete" ? "*" : action }.join(",")
|
48
|
-
[resource_type, resource_name, actions].join(":")
|
49
|
-
end
|
50
|
-
|
51
36
|
def authorization_auth
|
52
37
|
@authorization_auth ||= begin
|
53
38
|
if ::Dapp::Dapp.options_with_docker_credentials?
|
data/lib/dapp/helper/sha256.rb
CHANGED
@@ -35,12 +35,6 @@ module Dapp
|
|
35
35
|
'/apis/batch/v1' => [:job, ],
|
36
36
|
'/apis/batch/v1beta1' => [:cronjob, ],
|
37
37
|
},
|
38
|
-
'1.11' => {
|
39
|
-
'/api/v1' => [:service, :replicationcontroller, :pod, :podtemplate, ],
|
40
|
-
'/apis/apps/v1' => [:daemonset, :deployment, :replicaset, :statefulset, ],
|
41
|
-
'/apis/batch/v1' => [:job, ],
|
42
|
-
'/apis/batch/v1beta1' => [:cronjob, ],
|
43
|
-
},
|
44
38
|
'stable' => {
|
45
39
|
'/api/v1' => [:service, :replicationcontroller, :pod, :podtemplate, ],
|
46
40
|
'/apis/batch/v1' => [:job, ],
|
@@ -50,11 +50,12 @@ module Dapp
|
|
50
50
|
|
51
51
|
dapp.log_step("[#{Time.now}] Poll deployment '#{d.name}' status")
|
52
52
|
dapp.with_log_indent do
|
53
|
-
dapp.log_info("
|
54
|
-
dapp.log_info("Updated replicas: #{_field_value_for_log(d.status['updatedReplicas'])}")
|
55
|
-
dapp.log_info("Available replicas: #{_field_value_for_log(d.status['availableReplicas'])}")
|
56
|
-
dapp.log_info("Unavailable replicas: #{_field_value_for_log(d.status['unavailableReplicas'])}")
|
53
|
+
dapp.log_info("Target replicas: #{_field_value_for_log(d.replicas)}")
|
54
|
+
dapp.log_info("Updated replicas: #{_field_value_for_log(d.status['updatedReplicas'])} / #{_field_value_for_log(d.replicas)}")
|
55
|
+
dapp.log_info("Available replicas: #{_field_value_for_log(d.status['availableReplicas'])} / #{_field_value_for_log(d.replicas)}")
|
57
56
|
dapp.log_info("Ready replicas: #{_field_value_for_log(d.status['readyReplicas'])} / #{_field_value_for_log(d.replicas)}")
|
57
|
+
dapp.log_info("Old deployment.kubernetes.io/revision: #{_field_value_for_log(@revision_before_deploy)}")
|
58
|
+
dapp.log_info("Current deployment.kubernetes.io/revision: #{_field_value_for_log(d_revision)}")
|
58
59
|
end
|
59
60
|
|
60
61
|
rs = nil
|
@@ -83,16 +84,6 @@ module Dapp
|
|
83
84
|
end
|
84
85
|
|
85
86
|
if rs
|
86
|
-
dapp.with_log_indent do
|
87
|
-
dapp.log_step("Current ReplicaSet '#{rs.name}' status")
|
88
|
-
dapp.with_log_indent do
|
89
|
-
dapp.log_info("Replicas: #{_field_value_for_log(rs.status['replicas'])}")
|
90
|
-
dapp.log_info("Fully labeled replicas: #{_field_value_for_log(rs.status['fullyLabeledReplicas'])}")
|
91
|
-
dapp.log_info("Available replicas: #{_field_value_for_log(rs.status['availableReplicas'])}")
|
92
|
-
dapp.log_info("Ready replicas: #{_field_value_for_log(rs.status['readyReplicas'])} / #{_field_value_for_log(d.replicas)}")
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
87
|
# Pod'ы связанные с активным ReplicaSet
|
97
88
|
rs_pods = dapp.kubernetes.pod_list['items']
|
98
89
|
.map {|spec| Kubernetes::Client::Resource::Pod.new(spec)}
|
@@ -177,14 +168,19 @@ module Dapp
|
|
177
168
|
end # with_log_indent
|
178
169
|
end
|
179
170
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
171
|
+
if d_revision && d.replicas
|
172
|
+
if d.replicas == 0
|
173
|
+
break
|
174
|
+
else
|
175
|
+
break if begin
|
176
|
+
d.status['updatedReplicas'] &&
|
177
|
+
d.status['availableReplicas'] &&
|
178
|
+
d.status['readyReplicas'] &&
|
179
|
+
(d.status['updatedReplicas'] >= d.replicas) &&
|
180
|
+
(d.status['availableReplicas'] >= d.replicas) &&
|
181
|
+
(d.status['readyReplicas'] >= d.replicas)
|
182
|
+
end
|
183
|
+
end
|
188
184
|
end
|
189
185
|
|
190
186
|
sleep 5
|
@@ -193,14 +189,6 @@ module Dapp
|
|
193
189
|
end
|
194
190
|
end
|
195
191
|
|
196
|
-
def is_deployment_ready(d)
|
197
|
-
d.status.key?("readyReplicas") && d.status["readyReplicas"] >= d.replicas
|
198
|
-
end
|
199
|
-
|
200
|
-
def is_replicaset_ready(d, rs)
|
201
|
-
rs.status.key?("readyReplicas") && rs.status["readyReplicas"] >= d.replicas
|
202
|
-
end
|
203
|
-
|
204
192
|
private
|
205
193
|
|
206
194
|
def _field_value_for_log(value)
|
data/lib/dapp/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dapp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.32.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Stolyarov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-08-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mixlib-shellout
|
@@ -234,14 +234,14 @@ dependencies:
|
|
234
234
|
requirements:
|
235
235
|
- - "~>"
|
236
236
|
- !ruby/object:Gem::Version
|
237
|
-
version: '
|
237
|
+
version: '1.7'
|
238
238
|
type: :development
|
239
239
|
prerelease: false
|
240
240
|
version_requirements: !ruby/object:Gem::Requirement
|
241
241
|
requirements:
|
242
242
|
- - "~>"
|
243
243
|
- !ruby/object:Gem::Version
|
244
|
-
version: '
|
244
|
+
version: '1.7'
|
245
245
|
- !ruby/object:Gem::Dependency
|
246
246
|
name: rake
|
247
247
|
requirement: !ruby/object:Gem::Requirement
|
@@ -744,7 +744,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
744
744
|
version: 2.5.0
|
745
745
|
requirements: []
|
746
746
|
rubyforge_project:
|
747
|
-
rubygems_version: 2.7.
|
747
|
+
rubygems_version: 2.7.7
|
748
748
|
signing_key:
|
749
749
|
specification_version: 4
|
750
750
|
summary: Build docker packaged apps using chef or shell
|