brut 0.20.2 → 0.21.0.pre.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.
@@ -1,366 +1,337 @@
1
1
  require "brut/cli"
2
2
  require "fileutils"
3
3
  require "pathname"
4
+ require "yaml"
4
5
 
5
6
  class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
7
+
8
+ autoload :DeployConfig, "brut/cli/apps/deploy/deploy_config"
9
+ autoload :GitChecks, "brut/cli/apps/deploy/git_checks"
10
+
6
11
  def name = "deploy"
7
12
 
8
13
  def description = "Deploy your Brut-powered app to production"
9
14
 
10
15
  def default_rack_env = nil
11
16
 
12
- class Heroku < Brut::CLI::Commands::BaseCommand
13
- def description = "Deploy to Heroku using container-based deployment"
17
+ class Docker < Brut::CLI::Commands::BaseCommand
18
+ def description = "Build one docker image to use for all commands in production"
14
19
  def opts = [
15
- [ "--[no-]deploy", "If true, actually deploy the pushed images (default true)" ],
16
- [ "--skip-checks", "If true, skip pre-build checks" ],
20
+ [ "--build-only", "Only generate Dockerfiles and build images, do not deploy" ],
17
21
  ]
18
-
19
22
  def default_rack_env = "development"
20
-
21
23
  def run
22
- options.set_default(:deploy, true)
23
- puts "Logging in to Heroku Container Registry"
24
- command = %{heroku container:login}
25
- system!(command)
26
- execute_result = Brut::CLI::ExecuteResult.new do
27
- delegate_to_command(
28
- Brut::CLI::Apps::Deploy::Build.new(
29
- push: options.deploy? ? "registry.heroku.com/#{Brut.container.app_id}/%{name}": false
30
- )
31
- )
32
- end
33
- if execute_result.failed?
34
- puts theme.error.render("Build failed.")
35
- return execute_result.exit_status do |error_message|
36
- puts theme.error.render("Error message from build: #{error_message}")
24
+ deploy_config_path = Brut.container.project_root / "deploy" / "deploy_config.rb"
25
+ begin
26
+ require deploy_config_path
27
+ if !defined?(AppDeployConfig)
28
+ fatal "#{deploy_config_path} must define the class AppDeployConfig"
29
+ return 1
37
30
  end
38
- end
39
- names = []
40
- app_docker_files = AppDockerImages.new(
41
- project_root: Brut.container.project_root,
42
- organization: Brut.container.app_organization,
43
- app_id: Brut.container.app_id,
44
- short_version: "NA"
45
- )
46
- app_docker_files.each do |name:, cmd:, dockerfile:|
47
- names << name
48
- end
49
-
50
- deploy_command = "heroku container:release #{names.join(' ')} -a #{Brut.container.app_id}"
51
- if options.deploy?
52
- puts "Deploying images to Heroku"
53
- system!(deploy_command)
54
- else
55
- puts "Not deploying. To deploy the images just pushed:"
56
- puts ""
57
- puts " #{deploy_command}"
58
- end
59
- end
60
- end
61
-
62
- class AppDockerImages
63
- attr_reader :docker_config_filename, :platform
64
- def initialize(organization:, app_id:, project_root:, short_version:)
65
- @docker_config_filename = project_root / "deploy" / "docker_config"
66
- require_relative @docker_config_filename
67
- if ! defined?(DockerConfig)
68
- raise "#{@docker_config_filename} did not define the constant `DockerConfig` - it must to provide the configuration values to this script"
69
- end
70
-
71
- docker_config = DockerConfig.new
72
- @platform = docker_config.platform
73
-
74
- additional_images = (docker_config.additional_images || {}).map { |name,config|
75
- cmd = config.fetch(:cmd)
76
- image_name = %{#{Brut.container.app_organization}/#{Brut.container.app_id}:#{short_version}-#{name}}
77
- [
78
- name,
79
- {
80
- cmd:,
81
- image_name:,
82
- dockerfile: "deploy/Dockerfile.#{name}",
83
- },
84
- ]
85
- }.to_h
31
+ if !AppDeployConfig.ancestors.include?(Brut::CLI::Apps::Deploy::DeployConfig)
32
+ fatal "#{deploy_config_path} must define a subclass of Brut::CLI::Apps::Deploy::DeployConfig"
33
+ return 1
34
+ end
35
+ dockerfile = Brut.container.project_root / "deploy" / "Dockerfile"
36
+ if !dockerfile.exist?
37
+ fatal "#{dockerfile} does not exist - it should've been created when you added the Docker Deploy segment"
38
+ return 1
39
+ end
40
+ git_checks = Brut::CLI::Apps::Deploy::GitChecks.new(executor: execution_context.executor)
41
+ results = git_checks.check!
42
+ if results.errors?
43
+ results.errors.each do |_,message|
44
+ fatal message
45
+ end
46
+ return 1
47
+ end
48
+ version = ""
49
+ git_guess = %{git rev-parse HEAD}
50
+ system!(git_guess) do |output|
51
+ version << output
52
+ end
53
+ version = version.strip.chomp
54
+ if version == ""
55
+ fatal "Attempt to use git via command '#{git_guess}' to figure out the version failed"
56
+ return 1
57
+ end
58
+ short_version = version[0..7]
86
59
 
87
- @images = {
88
- "web" => {
89
- cmd: "bin/run",
90
- image_name: %{#{Brut.container.app_organization}/#{app_id}:#{short_version}-web},
91
- dockerfile: "deploy/Dockerfile.web",
92
- },
93
- "release" => {
94
- cmd: "bin/release",
95
- image_name: %{#{Brut.container.app_organization}/#{app_id}:#{short_version}-release},
96
- dockerfile: "deploy/Dockerfile.release",
97
- },
98
- }.merge(additional_images)
99
- end
60
+ config = AppDeployConfig.new
100
61
 
101
- def each(&block)
102
- if block.parameters.any? { it[0] != :keyreq }
103
- raise "block for #{self.class}#each must only contain required keyword parameters"
104
- end
105
- @images.each do |name,metadata|
106
- args = {}
107
- block.parameters.each do |(_,param)|
108
- if param == :name
109
- args[:name] = name
110
- else
111
- args[param] = metadata.fetch(param)
112
- end
62
+ image_name = %{#{Brut.container.app_organization}/#{Brut.container.app_id}:#{short_version}}
63
+ if config.registry_hostname
64
+ image_name = "#{config.registry_hostname}/#{image_name}"
65
+ end
66
+ dockerfile = Brut.container.project_root / "deploy" / "Dockerfile"
67
+ FileUtils.chdir Brut.container.project_root do
68
+ command = %{docker build --build-arg app_git_sha1=#{version} --file #{dockerfile} --platform #{config.platform} --tag #{image_name} . 2>&1}
69
+ system!(command)
70
+ end
71
+ if options.build_only?
72
+ puts "Not pushing image"
73
+ else
74
+ system!("docker image push #{image_name}")
113
75
  end
114
- block.(**args)
76
+
77
+ 0
78
+ rescue LoadError => ex
79
+ fatal "Could not find #{deploy_config_path}: #{ex}"
80
+ 1
115
81
  end
82
+
116
83
  end
117
84
  end
118
85
 
119
- class Build < Brut::CLI::Commands::BaseCommand
120
- def description = Docker.new.description
121
- def opts = Docker.new.opts
122
- def default_rack_env = Docker.new.default_rack_env
123
-
124
- def initialize(push: false)
125
- @push = push
126
- end
127
- def run
128
- delegate_to_command(Docker.new(push: @push))
129
- end
86
+ class Heroku < Brut::CLI::Commands::BaseCommand
87
+ class DeployConfig < Brut::CLI::Apps::Deploy::DeployConfig
130
88
 
131
- def commands = []
89
+ def registry_hostname = "registry.heroku.com"
132
90
 
133
- class Docker < Brut::CLI::Commands::BaseCommand
134
- def description = "Build a series of Docker images from a template Dockerfile"
135
- def opts = [
136
- [ "--platform=PLATFORM","Override default platform. Can be any Docker platform." ],
137
- [ "--dry-run", "Only show what would happen, don't actually do anything" ],
138
- [ "--skip-checks", "If true, skip pre-build checks (default )" ],
91
+ def processes = super + [
92
+ process_description("release", "bin/release")
139
93
  ]
140
- def default_rack_env = "development"
141
94
 
142
- def initialize(push: false)
143
- @push = push
95
+ def each_dockerfile(&block)
96
+ self.processes.each do |description|
97
+ dockerfile = "Dockerfile.#{description.name}"
98
+ block.(dockerfile, description)
99
+ end
144
100
  end
145
-
146
- def run
147
- if !options.skip_checks?
148
- execute_result = Brut::CLI::ExecuteResult.new do
149
- delegate_to_command(Brut::CLI::Apps::Deploy::Check.new)
150
- end
151
- if execute_result.failed?
152
- puts theme.error.render("Pre-build checks failed.")
153
- return execute_result.exit_status do |error_message|
154
- puts theme.error.render("Error message from checks: #{error_message}")
155
- end
101
+ end
102
+ def description = "Deploy to Heroku using container-based deployment"
103
+ def opts = [
104
+ [ "--build-only", "Only generate Dockerfiles and build images, do not deploy" ],
105
+ ]
106
+ def run
107
+ deploy_config_path = Brut.container.project_root / "deploy" / "deploy_config.rb"
108
+ begin
109
+ require deploy_config_path
110
+ if !defined?(AppDeployConfig)
111
+ fatal "#{deploy_config_path} must define the class AppDeployConfig"
112
+ return 1
113
+ end
114
+ if !AppDeployConfig.ancestors.include?(Brut::CLI::Apps::Deploy::Heroku::DeployConfig)
115
+ fatal "#{deploy_config_path} must define a subclass of Brut::CLI::Apps::Deploy::Heroku::DeployConfig"
116
+ return 1
117
+ end
118
+ dockerfile = Brut.container.project_root / "deploy" / "Dockerfile"
119
+ if !dockerfile.exist?
120
+ fatal "#{dockerfile} does not exist - it should've been created when you added the Heroku segment"
121
+ return 1
122
+ end
123
+ git_checks = Brut::CLI::Apps::Deploy::GitChecks.new(executor: execution_context.executor)
124
+ results = git_checks.check!
125
+ if results.errors?
126
+ results.errors.each do |_,message|
127
+ fatal message
156
128
  end
129
+ return 1
130
+ end
131
+ begin
132
+ command = %{heroku container:login}
133
+ system!(command)
134
+ rescue Brut::CLI::SystemExecError => ex
135
+ fatal(ex)
136
+ fatal("Not logged into Heroku")
137
+ return 1
157
138
  end
139
+ config = AppDeployConfig.new
158
140
  version = ""
159
141
  git_guess = %{git rev-parse HEAD}
160
142
  system!(git_guess) do |output|
161
143
  version << output
162
144
  end
163
- version.strip!.chomp!
145
+ version = version.strip.chomp
164
146
  if version == ""
165
- error "Attempt to use git via command '#{git_guess}' to figure out the version failed"
147
+ fatal "Attempt to use git via command '#{git_guess}' to figure out the version failed"
166
148
  return 1
167
149
  end
168
- short_version = version[0..7]
169
- app_docker_files = AppDockerImages.new(
170
- project_root: Brut.container.project_root,
171
- organization: Brut.container.app_organization,
172
- app_id: Brut.container.app_id,
173
- short_version:
174
- )
175
- options.set_default(:platform, app_docker_files.platform || "linux/amd64")
176
-
177
- FileUtils.chdir Brut.container.project_root do
178
-
179
- puts
180
- puts theme.header.render("Generating Dockerfiles")
181
- puts
182
- rows = []
183
- app_docker_files.each do |name:, cmd:, dockerfile:|
184
-
185
- rows << [theme.subheader.render(name), theme.code.render(dockerfile), theme.code.render(cmd) ]
186
-
187
- if !options.dry_run?
188
- File.open(dockerfile,"w") do |file|
189
- file.puts "# DO NOT EDIT - THIS IS GENERATED"
190
- file.puts "# To make changes, modifiy deploy/Dockerfile and run #{$0}"
191
- file.puts File.read("deploy/Dockerfile")
192
- file.puts
193
- file.puts "# Added by #{$0}"
194
- file.puts %{CMD [ "bundle", "exec", "#{cmd}" ]}
195
- end
196
- end
197
- end
198
- table = Lipgloss::Table.new.headers(["Name", "Dockerfile", "CMD"]).
199
- rows(rows).
200
- style_func(rows: rows.length, columns: 3) { Lipgloss::Style.new.padding_right(1).padding_left(1) }
201
- puts table.render
202
-
203
- puts
204
- puts theme.header.render("Images")
205
- puts
206
- rows = []
207
- items = []
208
- push_or_load = @push ? "--push" : "--load"
209
- app_docker_files.each do |name:, image_name:, dockerfile:|
210
- if @push && @push.kind_of?(String)
211
- image_name = @push % { name: name }
212
- end
213
- rows << [ name, theme.code.render(image_name) ]
214
- command = %{docker buildx build --provenance=false --build-arg app_git_sha1=#{version} --file #{Brut.container.project_root}/#{dockerfile} --platform #{options.platform} #{push_or_load} --tag #{image_name} . 2>&1}
215
- items << theme.code.render(theme.wrap(command, first_indent: false, indent: 7, newline: " \\\n"))
216
- if !options.dry_run?
217
- puts theme.subheader.render("Building #{@push ? 'and pushing' : '' } '#{name}' image")
218
- system!(command)
219
- end
220
- end
221
- if options.dry_run?
222
- table = Lipgloss::Table.new.headers(["Name", "Image Name" ]).
223
- rows(rows).
224
- style_func(rows: rows.length, columns: 3) { Lipgloss::Style.new.padding_right(1).padding_left(1) }
225
- puts table.render
226
- puts
227
- puts theme.subheader.render("Commands:")
228
- puts
229
- puts Lipgloss::List.new.items(items).item_style(theme.code).render
150
+ names = []
151
+ config.each_dockerfile do |process_dockerfile, process_description|
152
+ process_dockerfile_path = dockerfile.dirname / process_dockerfile
153
+ FileUtils.cp dockerfile, process_dockerfile_path
154
+ File.open(dockerfile.dirname / process_dockerfile, "a") do |file|
155
+ file.puts(process_description.cmd_directive)
230
156
  end
157
+ image_name = "#{config.registry_hostname}/#{Brut.container.app_id}/#{process_description.name}"
158
+ push_or_load = if options.build_only?
159
+ "--load"
160
+ else
161
+ "--push"
162
+ end
163
+ command = %{docker buildx build --provenance=false --build-arg app_git_sha1=#{version} --file #{process_dockerfile_path} --platform #{config.platform} #{push_or_load} --tag #{image_name} . 2>&1}
164
+ system!(command)
165
+ names << process_description.name
231
166
  end
167
+ deploy_command = "heroku container:release #{names.sort.join(' ')} -a #{Brut.container.app_id}"
168
+ if options.build_only?
169
+ puts "Not deploying"
170
+ else
171
+ system!(deploy_command)
172
+ end
173
+
174
+ 0
175
+ rescue LoadError => ex
176
+ fatal "Could not find #{deploy_config_path}: #{ex}"
177
+ 1
232
178
  end
233
179
  end
234
180
  end
235
-
181
+ module DockerCompose
236
182
  class Check < Brut::CLI::Commands::BaseCommand
237
-
238
- def description = "Check that a deploy can be reasonably expected to succeed"
239
-
240
- def opts = Git.new.opts
241
- def run = delegate_to_command(Git.new)
242
- def commands = []
243
-
244
- class Git < Brut::CLI::Commands::BaseCommand
245
- def description = "Perform the check assuming Git is the version-control system"
246
- def opts = [
247
- [ "--[no-]check-branch", "If true, requires that you are on 'main' (default true)" ],
248
- [ "--[no-]check-changes", "If true, requires that you have committed all local changes (default true)" ],
249
- [ "--[no-]check-push", "If true, requires that you are in sync with origin/main (default true)" ],
250
- ]
251
-
252
- def run
253
- puts theme.header.render("Checking Git repo to see if changes have all been pushed to main")
254
- puts
255
-
256
- options.set_default(:check_branch, true)
257
- options.set_default(:check_changes, true)
258
- options.set_default(:check_push, true)
259
-
260
- checks = []
261
-
262
- branch = ""
263
- system!("git branch --show-current") do |output|
264
- branch << output
265
- end
266
- branch = branch.strip.chomp
267
- checks << [
268
- "Deploy from main",
269
- ]
270
- if branch != "main"
271
- checks.last << "Currently on #{theme.code.render(branch)}"
272
- checks.last << options.check_branch?
273
- end
274
-
275
- system!("git status") do |*| # reset local caches to account for Docker/host wierdness
276
- # ignore
277
- end
278
- local_changes = ""
279
- system!("git diff-index --name-only HEAD --") do |output|
280
- local_changes << output
183
+ def description = "Check if the existing docker-compose.yml is consistent with the deploy config"
184
+ def run
185
+ docker_compose_path = Brut.container.project_root / "deploy" / "docker-compose.yml"
186
+ if !docker_compose_path.exist?
187
+ fatal "Could not find #{docker_compose_path}"
188
+ return 1
189
+ end
190
+ deploy_config_path = Brut.container.project_root / "deploy" / "deploy_config.rb"
191
+ begin
192
+ require deploy_config_path
193
+ if !defined?(AppDeployConfig)
194
+ fatal "#{deploy_config_path} must define the class AppDeployConfig"
195
+ return 1
281
196
  end
282
- checks << [
283
- "No un-committed changes",
284
- ]
285
- if local_changes.strip != ""
286
- items = local_changes.split(/\n/)
287
- list = Lipgloss::List.new.items(items).item_style(theme.error)
288
- checks.last << "Files not committed:\n#{list.render.strip}\n"
289
- checks.last << options.check_changes?
197
+ if !AppDeployConfig.ancestors.include?(Brut::CLI::Apps::Deploy::DeployConfig)
198
+ fatal "#{deploy_config_path} must define a subclass of Brut::CLI::Apps::Deploy::DeployConfig"
199
+ return 1
290
200
  end
291
-
292
- rev_list = ""
293
- system!("git rev-list --left-right --count origin/main...main") do |output|
294
- rev_list << output
201
+ config = AppDeployConfig.new
202
+ docker_compose_contents = YAML.load(File.read(docker_compose_path))
203
+ missing = []
204
+ extra = []
205
+ wrong = {}
206
+ failed = false
207
+ configured_services = []
208
+ expected_image_name = "#{Brut.container.app_organization}/#{Brut.container.app_id}:${DOCKER_IMAGE_TAG}"
209
+ if config.registry_hostname
210
+ expected_image_name = "#{config.registry_hostname}/#{expected_image_name}"
295
211
  end
296
- remote_ahead, local_ahead = rev_list.strip.chomp.split(/\t/,2).map(&:to_i)
297
- checks << [
298
- "Pulled from origin",
299
- ]
300
- if remote_ahead != 0
301
- if remote_ahead == 1
302
- checks.last << "There is 1 commit in origin you don't have"
212
+ config.processes.each do |process_description|
213
+ configured_services << process_description.name
214
+ service = docker_compose_contents["services"][process_description.name]
215
+ if service
216
+ image = service["image"]
217
+ cmd = service["command"]
218
+ if image != expected_image_name
219
+ wrong[process_description] ||= {}
220
+ wrong[process_description][:image] = {
221
+ expected: expected_image_name,
222
+ actual: image
223
+ }
224
+ failed = true
225
+ end
226
+ if cmd != process_description.cmd
227
+ wrong[process_description] ||= {}
228
+ wrong[process_description][:command] = {
229
+ expected: process_description.cmd,
230
+ actual: cmd
231
+ }
232
+ failed = true
233
+ end
303
234
  else
304
- checks.last << "There are #{remote_ahead} commits in origin you don't have"
235
+ missing << process_description
236
+ failed = true
305
237
  end
306
- checks.last << options.check_push?
307
238
  end
308
- checks << [
309
- "Pushed to origin",
310
- ]
311
-
312
- if local_ahead != 0
313
- if local_ahead == 1
314
- checks.last << "There is 1 commit not pushed to origin"
315
- else
316
- checks.last << "There are #{local_ahead} commits not pushed to origin"
239
+ docker_compose_contents["services"].each do |service_name,configuration|
240
+ if !configured_services.include?(service_name)
241
+ extra << service_name
242
+ failed = true
317
243
  end
318
- checks.last << options.check_push?
319
244
  end
320
-
321
- rows = []
322
- checks.each do |(check,status,error)|
323
- row = [ check ]
324
- if status
325
- if error
326
- row << theme.error.render("FAILED")
327
- else
328
- row << theme.warning.render("Ignored")
245
+ if failed
246
+ missing.each do |process_description|
247
+ fatal "service #{process_description.name}: MISSING"
248
+ end
249
+ wrong.each do |process_description, problems|
250
+ problems.each do |key,expected_actual|
251
+ fatal "service #{process_description.name}: #{key} incorrect. Expected '#{expected_actual[:expected]}', but got '#{expected_actual[:actual]}'"
329
252
  end
330
- row << theme.error.render(status)
331
- else
332
- row << theme.success.render("OK")
333
- row << ""
334
253
  end
335
- rows << row
336
- end
337
- table = Lipgloss::Table.new.
338
- headers(["Check", "Result", "Details"]).
339
- rows(rows).
340
- style_func(rows: rows.length, columns: 3) { |row,column|
341
- if row == Lipgloss::Table::HEADER_ROW
342
- Lipgloss::Style.new.inherit(theme.header).padding_left(1).padding_right(1)
343
- elsif column == 0
344
- Lipgloss::Style.new.inherit(theme.subheader).padding_left(1).padding_right(1).padding_bottom(1)
345
- else
346
- Lipgloss::Style.new.inherit(theme.none).padding_left(1).padding_right(1).padding_bottom(1)
254
+ extra.each do |service_name|
255
+ fatal "service #{service_name}: not in deploy config"
347
256
  end
348
- }
349
- puts table.render
350
- checks_failed = checks.count { |(_,status,_)| status }
351
- checks_failed_not_ignored = checks.count { |(_,status,error)| status && error }
352
- if checks_failed > 0
353
- if checks_failed_not_ignored > 0
354
- puts theme.error.render("#{checks_failed} checks failed - aborting")
355
257
  return 1
356
- else
357
- puts theme.warning.render("#{checks_failed} checks failed but ignored")
358
258
  end
359
- else
360
- puts theme.success.render("All checks passed")
259
+ 0
260
+ rescue LoadError => ex
261
+ fatal "Could not find #{deploy_config_path}: #{ex}"
262
+ 1
361
263
  end
264
+ end
265
+ end
266
+ class Generate < Brut::CLI::Commands::BaseCommand
267
+ def description = "Generate or update the existing docker-compose.yml based on current deploy config"
268
+ def run
269
+ docker_compose_path = Brut.container.project_root / "deploy" / "docker-compose.yml"
270
+ deploy_config_path = Brut.container.project_root / "deploy" / "deploy_config.rb"
271
+ begin
272
+ require deploy_config_path
273
+ if !defined?(AppDeployConfig)
274
+ fatal "#{deploy_config_path} must define the class AppDeployConfig"
275
+ return 1
276
+ end
277
+ if !AppDeployConfig.ancestors.include?(Brut::CLI::Apps::Deploy::DeployConfig)
278
+ fatal "#{deploy_config_path} must define a subclass of Brut::CLI::Apps::Deploy::DeployConfig"
279
+ return 1
280
+ end
281
+ config = AppDeployConfig.new
282
+ yaml_contents = if docker_compose_path.exist?
283
+ YAML.load(File.read(docker_compose_path))
284
+ else
285
+ {}
286
+ end
287
+ yaml_contents["services"] ||= {}
288
+
289
+ image_name = "#{Brut.container.app_organization}/#{Brut.container.app_id}:${DOCKER_IMAGE_TAG}"
290
+ if config.registry_hostname
291
+ image_name = "#{config.registry_hostname}/#{image_name}"
292
+ end
293
+ configured_services = []
294
+ config.processes.each do |process_description|
295
+ configured_services << process_description.name
296
+ existing = yaml_contents["services"][process_description.name]
297
+ if !existing
298
+ puts "Creating configuration for '#{process_description.name}'"
299
+ existing = {
300
+ "env_file" => "/etc/#{Brut.container.app_id}/env",
301
+ "extra_hosts" => [
302
+ "host.docker.internal:host-gateway",
303
+ ],
304
+ "restart" => "unless-stopped",
305
+ }
306
+ if process_description.name == "web"
307
+ existing["ports"] = [
308
+ "127.0.0.1:6502:6502",
309
+ ]
310
+ end
311
+ else
312
+ puts "Updating image and command for '#{process_description.name}'"
313
+ end
314
+ existing["image"] = image_name
315
+ existing["command"] = process_description.cmd
316
+ yaml_contents["services"][process_description.name] = existing
317
+ end
318
+ trimmed_services = yaml_contents["services"].select { |service_name, service_configuration|
319
+ configured_services.include?(service_name).tap { |exists|
320
+ if !exists
321
+ puts "Removing configuration for '#{service_name}'"
322
+ end
323
+ }
324
+ }.to_h
325
+ yaml_contents["services"] = trimmed_services
362
326
 
363
- 0
327
+ File.open(docker_compose_path,"w") do |file|
328
+ file.puts YAML.dump(yaml_contents)
329
+ end
330
+ 0
331
+ rescue LoadError => ex
332
+ fatal "Could not find #{deploy_config_path}: #{ex}"
333
+ 1
334
+ end
364
335
  end
365
336
  end
366
337
  end
@@ -28,6 +28,11 @@ class Brut::CLI::Apps::New
28
28
  project_root: add_segment_options.project_root,
29
29
  templates_dir:
30
30
  )
31
+ elsif @add_segment_options.segment_name == "docker-deploy"
32
+ Brut::CLI::Apps::New::Segments::DockerDeploy.new(
33
+ project_root: add_segment_options.project_root,
34
+ templates_dir:
35
+ )
31
36
  end
32
37
  end
33
38