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.
- checksums.yaml +4 -4
- data/lib/brut/cli/apps/deploy/deploy_config.rb +62 -0
- data/lib/brut/cli/apps/deploy/git_checks.rb +54 -0
- data/lib/brut/cli/apps/deploy.rb +276 -307
- data/lib/brut/cli/apps/new/add_segment.rb +5 -0
- data/lib/brut/cli/apps/new/app.rb +10 -0
- data/lib/brut/cli/apps/new/segments/docker_deploy.rb +29 -0
- data/lib/brut/cli/apps/new/segments/sidekiq.rb +8 -8
- data/lib/brut/cli/apps/new/segments.rb +1 -0
- data/lib/brut/cli/commands/base_command.rb +28 -8
- data/lib/brut/framework/app.rb +2 -1
- data/lib/brut/framework/config.rb +13 -1
- data/lib/brut/framework/mcp.rb +1 -4
- data/lib/brut/front_end/csrf_protector.rb +10 -6
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +1 -1
- data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +2 -0
- data/lib/brut/front_end/routing.rb +22 -0
- data/lib/brut/instrumentation/open_telemetry.rb +5 -0
- data/lib/brut/sinatra_helpers.rb +16 -4
- data/lib/brut/spec_support/cli_command_support.rb +67 -7
- data/lib/brut/spec_support/matchers/have_executed.rb +2 -0
- data/lib/brut/version.rb +1 -1
- data/templates/Base/dx/exec +10 -3
- data/templates/segments/DockerDeploy/deploy/Dockerfile +126 -0
- data/templates/segments/DockerDeploy/deploy/deploy_config.rb +17 -0
- data/templates/segments/DockerDeploy/deploy/docker-entrypoint +15 -0
- data/templates/segments/Heroku/deploy/deploy_config.rb +21 -0
- metadata +8 -3
- data/lib/brut/cli/apps/new/old_app.rb +0 -81
- data/templates/segments/Heroku/deploy/docker_config.rb +0 -30
data/lib/brut/cli/apps/deploy.rb
CHANGED
|
@@ -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
|
|
13
|
-
def description = "
|
|
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
|
-
[ "--
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
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
|
-
|
|
86
|
+
def registry_hostname = "registry.heroku.com"
|
|
132
87
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
161
|
-
version << output
|
|
162
|
-
end
|
|
163
|
-
version.strip!.chomp!
|
|
139
|
+
version = capture!(git_guess).strip.chomp
|
|
164
140
|
if version == ""
|
|
165
|
-
|
|
141
|
+
fatal "Attempt to use git via command '#{git_guess}' to figure out the version failed"
|
|
166
142
|
return 1
|
|
167
143
|
end
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
283
|
-
"
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
232
|
+
missing << process_description
|
|
233
|
+
failed = true
|
|
305
234
|
end
|
|
306
|
-
checks.last << options.check_push?
|
|
307
235
|
end
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|