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.
- 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 +275 -304
- 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 +30 -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 +6 -2
- 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 +60 -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,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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
86
|
+
class Heroku < Brut::CLI::Commands::BaseCommand
|
|
87
|
+
class DeployConfig < Brut::CLI::Apps::Deploy::DeployConfig
|
|
130
88
|
|
|
131
|
-
|
|
89
|
+
def registry_hostname = "registry.heroku.com"
|
|
132
90
|
|
|
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 )" ],
|
|
91
|
+
def processes = super + [
|
|
92
|
+
process_description("release", "bin/release")
|
|
139
93
|
]
|
|
140
|
-
def default_rack_env = "development"
|
|
141
94
|
|
|
142
|
-
def
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
145
|
+
version = version.strip.chomp
|
|
164
146
|
if version == ""
|
|
165
|
-
|
|
147
|
+
fatal "Attempt to use git via command '#{git_guess}' to figure out the version failed"
|
|
166
148
|
return 1
|
|
167
149
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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?
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
235
|
+
missing << process_description
|
|
236
|
+
failed = true
|
|
305
237
|
end
|
|
306
|
-
checks.last << options.check_push?
|
|
307
238
|
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"
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|