brut 0.20.2 → 0.21.0.pre.2

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,335 @@
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
+ version = capture!(git_guess).strip.chomp
51
+ if version == ""
52
+ fatal "Attempt to use git via command '#{git_guess}' to figure out the version failed"
53
+ return 1
54
+ end
55
+ short_version = version[0..7]
86
56
 
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
57
+ config = AppDeployConfig.new
100
58
 
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
59
+ image_name = %{#{Brut.container.app_organization}/#{Brut.container.app_id}:#{short_version}}
60
+ if config.registry_hostname
61
+ image_name = "#{config.registry_hostname}/#{image_name}"
62
+ end
63
+ dockerfile = Brut.container.project_root / "deploy" / "Dockerfile"
64
+ FileUtils.chdir Brut.container.project_root do
65
+ command = %{docker build --build-arg app_git_sha1=#{version} --file #{dockerfile} --platform #{config.platform} --tag #{image_name} . 2>&1}
66
+ system!(command, output: :stream)
67
+ end
68
+ if options.build_only?
69
+ puts "Not pushing image"
70
+ else
71
+ system!("docker image push #{image_name}", output: :stream)
113
72
  end
114
- block.(**args)
73
+
74
+ 0
75
+ rescue LoadError => ex
76
+ fatal "Could not find #{deploy_config_path}: #{ex}"
77
+ 1
115
78
  end
79
+
116
80
  end
117
81
  end
118
82
 
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
83
+ class Heroku < Brut::CLI::Commands::BaseCommand
84
+ class DeployConfig < Brut::CLI::Apps::Deploy::DeployConfig
130
85
 
131
- def commands = []
86
+ def registry_hostname = "registry.heroku.com"
132
87
 
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 )" ],
88
+ def processes = super + [
89
+ process_description("release", "bin/release")
139
90
  ]
140
- def default_rack_env = "development"
141
91
 
142
- def initialize(push: false)
143
- @push = push
92
+ def each_dockerfile(&block)
93
+ self.processes.each do |description|
94
+ dockerfile = "Dockerfile.#{description.name}"
95
+ block.(dockerfile, description)
96
+ end
144
97
  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
98
+ end
99
+ def description = "Deploy to Heroku using container-based deployment"
100
+ def opts = [
101
+ [ "--build-only", "Only generate Dockerfiles and build images, do not deploy" ],
102
+ ]
103
+ def run
104
+ deploy_config_path = Brut.container.project_root / "deploy" / "deploy_config.rb"
105
+ begin
106
+ require deploy_config_path
107
+ if !defined?(AppDeployConfig)
108
+ fatal "#{deploy_config_path} must define the class AppDeployConfig"
109
+ return 1
110
+ end
111
+ if !AppDeployConfig.ancestors.include?(Brut::CLI::Apps::Deploy::Heroku::DeployConfig)
112
+ fatal "#{deploy_config_path} must define a subclass of Brut::CLI::Apps::Deploy::Heroku::DeployConfig"
113
+ return 1
114
+ end
115
+ dockerfile = Brut.container.project_root / "deploy" / "Dockerfile"
116
+ if !dockerfile.exist?
117
+ fatal "#{dockerfile} does not exist - it should've been created when you added the Heroku segment"
118
+ return 1
119
+ end
120
+ git_checks = Brut::CLI::Apps::Deploy::GitChecks.new(executor: execution_context.executor)
121
+ results = git_checks.check!
122
+ if results.errors?
123
+ results.errors.each do |_,message|
124
+ fatal message
156
125
  end
126
+ return 1
127
+ end
128
+ begin
129
+ command = %{heroku container:login}
130
+ system!(command)
131
+ rescue Brut::CLI::SystemExecError => ex
132
+ fatal(ex)
133
+ fatal("Not logged into Heroku")
134
+ return 1
157
135
  end
136
+ config = AppDeployConfig.new
158
137
  version = ""
159
138
  git_guess = %{git rev-parse HEAD}
160
- system!(git_guess) do |output|
161
- version << output
162
- end
163
- version.strip!.chomp!
139
+ version = capture!(git_guess).strip.chomp
164
140
  if version == ""
165
- error "Attempt to use git via command '#{git_guess}' to figure out the version failed"
141
+ fatal "Attempt to use git via command '#{git_guess}' to figure out the version failed"
166
142
  return 1
167
143
  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
144
+ names = []
145
+ config.each_dockerfile do |process_dockerfile, process_description|
146
+ process_dockerfile_path = dockerfile.dirname / process_dockerfile
147
+ FileUtils.cp dockerfile, process_dockerfile_path
148
+ File.open(dockerfile.dirname / process_dockerfile, "a") do |file|
149
+ file.puts(process_description.cmd_directive)
230
150
  end
151
+ image_name = "#{config.registry_hostname}/#{Brut.container.app_id}/#{process_description.name}"
152
+ push_or_load = if options.build_only?
153
+ "--load"
154
+ else
155
+ "--push"
156
+ end
157
+ 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}
158
+ system!(command, output: :stream)
159
+ names << process_description.name
160
+ end
161
+ deploy_command = "heroku container:release #{names.sort.join(' ')} -a #{Brut.container.app_id}"
162
+ if options.build_only?
163
+ puts "Not deploying"
164
+ else
165
+ system!(deploy_command, output: :stream)
231
166
  end
167
+
168
+ 0
169
+ rescue LoadError => ex
170
+ fatal "Could not find #{deploy_config_path}: #{ex}"
171
+ 1
232
172
  end
233
173
  end
234
174
  end
235
-
175
+ class DockerCompose < Brut::CLI::Commands::BaseCommand
176
+ def default_rack_env = "development"
177
+ def description = "Manage a docker-compose.yml file to be consistent with your deploy config"
236
178
  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
179
+ def description = "Check if the existing docker-compose.yml is consistent with the deploy config"
180
+ def default_rack_env = "development"
181
+ def run
182
+ docker_compose_path = Brut.container.project_root / "deploy" / "docker-compose.yml"
183
+ if !docker_compose_path.exist?
184
+ fatal "Could not find #{docker_compose_path}"
185
+ return 1
186
+ end
187
+ deploy_config_path = Brut.container.project_root / "deploy" / "deploy_config.rb"
188
+ begin
189
+ require deploy_config_path
190
+ if !defined?(AppDeployConfig)
191
+ fatal "#{deploy_config_path} must define the class AppDeployConfig"
192
+ return 1
281
193
  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?
194
+ if !AppDeployConfig.ancestors.include?(Brut::CLI::Apps::Deploy::DeployConfig)
195
+ fatal "#{deploy_config_path} must define a subclass of Brut::CLI::Apps::Deploy::DeployConfig"
196
+ return 1
290
197
  end
291
-
292
- rev_list = ""
293
- system!("git rev-list --left-right --count origin/main...main") do |output|
294
- rev_list << output
198
+ config = AppDeployConfig.new
199
+ docker_compose_contents = YAML.load(File.read(docker_compose_path))
200
+ missing = []
201
+ extra = []
202
+ wrong = {}
203
+ failed = false
204
+ configured_services = []
205
+ expected_image_name = "#{Brut.container.app_organization}/#{Brut.container.app_id}:${DOCKER_IMAGE_TAG}"
206
+ if config.registry_hostname
207
+ expected_image_name = "#{config.registry_hostname}/#{expected_image_name}"
295
208
  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"
209
+ config.processes.each do |process_description|
210
+ configured_services << process_description.name
211
+ service = docker_compose_contents["services"][process_description.name]
212
+ if service
213
+ image = service["image"]
214
+ cmd = service["command"]
215
+ if image != expected_image_name
216
+ wrong[process_description] ||= {}
217
+ wrong[process_description][:image] = {
218
+ expected: expected_image_name,
219
+ actual: image
220
+ }
221
+ failed = true
222
+ end
223
+ if cmd != process_description.cmd
224
+ wrong[process_description] ||= {}
225
+ wrong[process_description][:command] = {
226
+ expected: process_description.cmd,
227
+ actual: cmd
228
+ }
229
+ failed = true
230
+ end
303
231
  else
304
- checks.last << "There are #{remote_ahead} commits in origin you don't have"
232
+ missing << process_description
233
+ failed = true
305
234
  end
306
- checks.last << options.check_push?
307
235
  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"
236
+ docker_compose_contents["services"].each do |service_name,configuration|
237
+ if !configured_services.include?(service_name)
238
+ extra << service_name
239
+ failed = true
317
240
  end
318
- checks.last << options.check_push?
319
241
  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")
242
+ if failed
243
+ missing.each do |process_description|
244
+ fatal "service #{process_description.name}: MISSING"
245
+ end
246
+ wrong.each do |process_description, problems|
247
+ problems.each do |key,expected_actual|
248
+ fatal "service #{process_description.name}: #{key} incorrect. Expected '#{expected_actual[:expected]}', but got '#{expected_actual[:actual]}'"
329
249
  end
330
- row << theme.error.render(status)
331
- else
332
- row << theme.success.render("OK")
333
- row << ""
334
250
  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)
251
+ extra.each do |service_name|
252
+ fatal "service #{service_name}: not in deploy config"
347
253
  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
254
  return 1
356
- else
357
- puts theme.warning.render("#{checks_failed} checks failed but ignored")
358
255
  end
359
- else
360
- puts theme.success.render("All checks passed")
256
+ 0
257
+ rescue LoadError => ex
258
+ fatal "Could not find #{deploy_config_path}: #{ex}"
259
+ 1
361
260
  end
261
+ end
262
+ end
263
+ class Generate < Brut::CLI::Commands::BaseCommand
264
+ def description = "Generate or update the existing docker-compose.yml based on current deploy config"
265
+ def default_rack_env = "development"
266
+ def run
267
+ docker_compose_path = Brut.container.project_root / "deploy" / "docker-compose.yml"
268
+ deploy_config_path = Brut.container.project_root / "deploy" / "deploy_config.rb"
269
+ begin
270
+ require deploy_config_path
271
+ if !defined?(AppDeployConfig)
272
+ fatal "#{deploy_config_path} must define the class AppDeployConfig"
273
+ return 1
274
+ end
275
+ if !AppDeployConfig.ancestors.include?(Brut::CLI::Apps::Deploy::DeployConfig)
276
+ fatal "#{deploy_config_path} must define a subclass of Brut::CLI::Apps::Deploy::DeployConfig"
277
+ return 1
278
+ end
279
+ config = AppDeployConfig.new
280
+ yaml_contents = if docker_compose_path.exist?
281
+ YAML.load(File.read(docker_compose_path))
282
+ else
283
+ {}
284
+ end
285
+ yaml_contents["services"] ||= {}
286
+
287
+ image_name = "#{Brut.container.app_organization}/#{Brut.container.app_id}:${DOCKER_IMAGE_TAG}"
288
+ if config.registry_hostname
289
+ image_name = "#{config.registry_hostname}/#{image_name}"
290
+ end
291
+ configured_services = []
292
+ config.processes.each do |process_description|
293
+ configured_services << process_description.name
294
+ existing = yaml_contents["services"][process_description.name]
295
+ if !existing
296
+ puts "Creating configuration for '#{process_description.name}'"
297
+ existing = {
298
+ "env_file" => "/etc/#{Brut.container.app_id}/env",
299
+ "extra_hosts" => [
300
+ "host.docker.internal:host-gateway",
301
+ ],
302
+ "restart" => "unless-stopped",
303
+ }
304
+ if process_description.name == "web"
305
+ existing["ports"] = [
306
+ "127.0.0.1:6502:6502",
307
+ ]
308
+ end
309
+ else
310
+ puts "Updating image and command for '#{process_description.name}'"
311
+ end
312
+ existing["image"] = image_name
313
+ existing["command"] = process_description.cmd
314
+ yaml_contents["services"][process_description.name] = existing
315
+ end
316
+ trimmed_services = yaml_contents["services"].select { |service_name, service_configuration|
317
+ configured_services.include?(service_name).tap { |exists|
318
+ if !exists
319
+ puts "Removing configuration for '#{service_name}'"
320
+ end
321
+ }
322
+ }.to_h
323
+ yaml_contents["services"] = trimmed_services
362
324
 
363
- 0
325
+ File.open(docker_compose_path,"w") do |file|
326
+ file.puts YAML.dump(yaml_contents)
327
+ end
328
+ 0
329
+ rescue LoadError => ex
330
+ fatal "Could not find #{deploy_config_path}: #{ex}"
331
+ 1
332
+ end
364
333
  end
365
334
  end
366
335
  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