dash 4.0.8 → 4.1.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/dash/build/progress_parser.rb +136 -0
- data/lib/dash/build/report.rb +104 -0
- data/lib/dash/build/step.rb +49 -0
- data/lib/dash/cli/app/boot.rb +46 -24
- data/lib/dash/cli/app.rb +4 -4
- data/lib/dash/cli/base.rb +117 -2
- data/lib/dash/cli/build.rb +83 -20
- data/lib/dash/cli/doctor/config_checks.rb +36 -1
- data/lib/dash/cli/doctor.rb +2 -1
- data/lib/dash/cli/healthcheck/poller.rb +15 -3
- data/lib/dash/cli/healthcheck/progress_reporter.rb +39 -0
- data/lib/dash/cli/main.rb +24 -7
- data/lib/dash/cli/proxy/drift.rb +17 -2
- data/lib/dash/cli/proxy/legacy_rename.rb +8 -21
- data/lib/dash/cli/proxy/loadbalancer_reboot.rb +8 -1
- data/lib/dash/cli/proxy/reboot.rb +6 -1
- data/lib/dash/cli/proxy.rb +19 -12
- data/lib/dash/cli/prune.rb +5 -8
- data/lib/dash/cli/report.rb +97 -0
- data/lib/dash/cli/templates/sample_hooks/post-deploy.sample +5 -0
- data/lib/dash/commander.rb +10 -2
- data/lib/dash/commands/app.rb +81 -0
- data/lib/dash/commands/auditor.rb +10 -0
- data/lib/dash/commands/base.rb +42 -1
- data/lib/dash/commands/builder/base.rb +18 -0
- data/lib/dash/commands/builder.rb +1 -1
- data/lib/dash/commands/loadbalancer.rb +54 -0
- data/lib/dash/commands/proxy/state.rb +31 -0
- data/lib/dash/commands/proxy.rb +74 -2
- data/lib/dash/commands/registry.rb +14 -0
- data/lib/dash/configuration/docs/configuration.yml +6 -0
- data/lib/dash/configuration/docs/report.yml +39 -0
- data/lib/dash/configuration/docs/role.yml +7 -7
- data/lib/dash/configuration/proxy.rb +3 -0
- data/lib/dash/configuration/report.rb +65 -0
- data/lib/dash/configuration.rb +2 -1
- data/lib/dash/dockerfile/analyzer.rb +66 -0
- data/lib/dash/dockerfile/context.rb +147 -0
- data/lib/dash/dockerfile/dockerignore.rb +29 -0
- data/lib/dash/dockerfile/document.rb +28 -0
- data/lib/dash/dockerfile/finding.rb +20 -0
- data/lib/dash/dockerfile/hadolint.rb +75 -0
- data/lib/dash/dockerfile/instruction.rb +58 -0
- data/lib/dash/dockerfile/parser.rb +199 -0
- data/lib/dash/dockerfile/rules/apt_hygiene.rb +35 -0
- data/lib/dash/dockerfile/rules/base.rb +44 -0
- data/lib/dash/dockerfile/rules/cache_busting_arg.rb +40 -0
- data/lib/dash/dockerfile/rules/cache_export_cost.rb +20 -0
- data/lib/dash/dockerfile/rules/context_size.rb +20 -0
- data/lib/dash/dockerfile/rules/copy_before_install.rb +34 -0
- data/lib/dash/dockerfile/rules/curl_pipe_shell.rb +16 -0
- data/lib/dash/dockerfile/rules/dockerignore_gaps.rb +36 -0
- data/lib/dash/dockerfile/rules/inline_env_blob.rb +23 -0
- data/lib/dash/dockerfile/rules/latest_base.rb +24 -0
- data/lib/dash/dockerfile/rules/missing_dockerignore.rb +11 -0
- data/lib/dash/dockerfile/rules/no_cache_mount.rb +26 -0
- data/lib/dash/dockerfile/rules/root_user.rb +12 -0
- data/lib/dash/dockerfile/rules/secret_in_build_arg.rb +30 -0
- data/lib/dash/dockerfile/rules/single_stage_build_deps.rb +19 -0
- data/lib/dash/dockerfile/rules/uncached_install.rb +20 -0
- data/lib/dash/dockerfile/stage.rb +65 -0
- data/lib/dash/otel_shipper.rb +5 -4
- data/lib/dash/output/otel_logger.rb +52 -0
- data/lib/dash/report/history.rb +94 -0
- data/lib/dash/report/trends.rb +129 -0
- data/lib/dash/report/writer.rb +142 -0
- data/lib/dash/report.rb +170 -0
- data/lib/dash/sshkit_with_ext.rb +62 -0
- data/lib/dash/timings.rb +163 -10
- data/lib/dash/utils.rb +7 -0
- data/lib/dash/version.rb +1 -1
- data/lib/dash.rb +4 -0
- metadata +38 -1
data/lib/dash/cli/build.rb
CHANGED
|
@@ -37,6 +37,9 @@ class Dash::Cli::Build < Dash::Cli::Base
|
|
|
37
37
|
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
parser = build_progress_parser
|
|
41
|
+
handler = handler_for(parser)
|
|
42
|
+
|
|
40
43
|
forward_local_registry_port_for_remote_builder do
|
|
41
44
|
with_env(DASH.config.builder.secrets) do
|
|
42
45
|
run_locally do
|
|
@@ -60,19 +63,19 @@ class Dash::Cli::Build < Dash::Cli::Base
|
|
|
60
63
|
push = DASH.builder.push(cli.options[:output], no_cache: cli.options[:no_cache])
|
|
61
64
|
|
|
62
65
|
DASH.with_verbosity(:debug) do
|
|
63
|
-
Dir.chdir(DASH.config.builder.build_directory) { execute *push, env: DASH.builder.push_env }
|
|
66
|
+
Dir.chdir(DASH.config.builder.build_directory) { execute *push, env: DASH.builder.push_env, **handler }
|
|
64
67
|
end
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
71
|
+
ensure
|
|
72
|
+
record_build_report parser
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
desc "pull", "Pull app image from registry onto servers"
|
|
71
76
|
def pull
|
|
72
|
-
login_to_registry_remotely unless DASH.registry.local?
|
|
73
|
-
|
|
74
77
|
forward_local_registry_port(DASH.hosts, **DASH.config.ssh.options) do
|
|
75
|
-
if (first_hosts =
|
|
78
|
+
if (first_hosts = login_and_mirror_hosts).any?
|
|
76
79
|
# Pull on a single host per mirror first to seed them
|
|
77
80
|
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
|
|
78
81
|
pull_on_hosts(first_hosts)
|
|
@@ -147,17 +150,66 @@ class Dash::Cli::Build < Dash::Cli::Base
|
|
|
147
150
|
say
|
|
148
151
|
end
|
|
149
152
|
|
|
153
|
+
parser = build_progress_parser
|
|
154
|
+
handler = handler_for(parser)
|
|
155
|
+
|
|
150
156
|
with_env(DASH.config.builder.secrets) do
|
|
151
157
|
run_locally do
|
|
152
158
|
build = DASH.builder.push(cli.options[:output], tag_as_dirty: true, no_cache: cli.options[:no_cache])
|
|
153
159
|
DASH.with_verbosity(:debug) do
|
|
154
|
-
execute(*build)
|
|
160
|
+
execute(*build, **handler)
|
|
155
161
|
end
|
|
156
162
|
end
|
|
157
163
|
end
|
|
164
|
+
ensure
|
|
165
|
+
# `dev` builds the working directory even when `push` would clone, so the advice reads
|
|
166
|
+
# the Dockerfile here rather than in a clone that may not exist.
|
|
167
|
+
record_build_report parser, build_directory: "."
|
|
158
168
|
end
|
|
159
169
|
|
|
160
170
|
private
|
|
171
|
+
# Buildpacks print a total and nothing else, so there is nothing for the parser to
|
|
172
|
+
# read and no reason to attach it.
|
|
173
|
+
def build_progress_parser
|
|
174
|
+
Dash::Build::ProgressParser.new unless DASH.builder.pack?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def handler_for(parser)
|
|
178
|
+
parser ? { interaction_handler: parser } : {}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Runs whether the build succeeded or failed: a partial report naming the step that
|
|
182
|
+
# broke is exactly what an operator wants from a failed build. Measurement must never
|
|
183
|
+
# be the reason a build fails, so nothing in here is allowed to raise.
|
|
184
|
+
def record_build_report(parser, build_directory: DASH.config.builder.build_directory)
|
|
185
|
+
return unless parser
|
|
186
|
+
|
|
187
|
+
guarded_report do
|
|
188
|
+
parser.finish
|
|
189
|
+
DASH.report.build = parser.result
|
|
190
|
+
|
|
191
|
+
say "Deploy report unavailable: #{parser.error.class}: #{parser.error.message}", :yellow if parser.error
|
|
192
|
+
|
|
193
|
+
# Inside a deploy the phase table prints these at the end and the deploy runs its
|
|
194
|
+
# own analysis once the image is delivered. Standalone, this is the only chance.
|
|
195
|
+
unless DASH.report.build_entry
|
|
196
|
+
print_build_report
|
|
197
|
+
DASH.report.analyze!(DASH.config, build_directory: build_directory)
|
|
198
|
+
puts DASH.report.advice_lines
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# A standalone `dash build push` has no phase table to sit under, but a CI pipeline
|
|
204
|
+
# that splits build from deploy should still see where the build time went.
|
|
205
|
+
def print_build_report
|
|
206
|
+
rows = DASH.report.build_lines
|
|
207
|
+
return if rows.empty?
|
|
208
|
+
|
|
209
|
+
puts " Build"
|
|
210
|
+
puts rows
|
|
211
|
+
end
|
|
212
|
+
|
|
161
213
|
def connect_to_remote_host(remote_host)
|
|
162
214
|
remote_uri = URI.parse(remote_host)
|
|
163
215
|
if remote_uri.scheme == "ssh"
|
|
@@ -171,26 +223,37 @@ class Dash::Cli::Build < Dash::Cli::Base
|
|
|
171
223
|
end
|
|
172
224
|
end
|
|
173
225
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
[]
|
|
226
|
+
# The registry login and the mirror probe share one round trip per host. The probe only
|
|
227
|
+
# earns its keep where there is more than one app host to seed, so on a single host the
|
|
228
|
+
# login goes on its own; a local registry needs no login and the fold is the probe alone.
|
|
229
|
+
#
|
|
230
|
+
# A host with no mirror configured fails the `docker info` half with docker's own index
|
|
231
|
+
# error, which is what "no mirror" looks like. Anything else still raises - a rejected
|
|
232
|
+
# login short-circuits the `&&` and comes back with docker's `unauthorized`/`denied`,
|
|
233
|
+
# which this rescue does not match.
|
|
234
|
+
def login_and_mirror_hosts
|
|
235
|
+
unless DASH.app_hosts.many?
|
|
236
|
+
login_to_registry_remotely unless DASH.registry.local?
|
|
237
|
+
return []
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
mirror_hosts = Concurrent::Hash.new
|
|
241
|
+
on(DASH.app_hosts) do |host|
|
|
242
|
+
first_mirror = capture_with_info(*DASH.registry.login_then(DASH.builder.first_mirror)).strip.presence
|
|
243
|
+
mirror_hosts[first_mirror] ||= host.to_s if first_mirror
|
|
244
|
+
rescue SSHKit::Command::Failed => e
|
|
245
|
+
raise unless e.message =~ /error calling index: reflect: slice index out of range/
|
|
186
246
|
end
|
|
247
|
+
mirror_hosts.values
|
|
187
248
|
end
|
|
188
249
|
|
|
250
|
+
# Audit, clean and pull share one round trip. validate_image keeps its own: folding it
|
|
251
|
+
# in would put the pull under validate_image's trailing `|| (echo ... && exit 1)`, and
|
|
252
|
+
# a failed pull would then report a missing service label.
|
|
189
253
|
def pull_on_hosts(hosts)
|
|
190
254
|
on(hosts) do
|
|
191
|
-
execute *DASH.auditor.
|
|
192
|
-
|
|
193
|
-
execute *DASH.builder.pull
|
|
255
|
+
execute *DASH.auditor.record_then("Pulled image with version #{DASH.config.version}",
|
|
256
|
+
DASH.builder.clean_then_pull)
|
|
194
257
|
execute *DASH.builder.validate_image
|
|
195
258
|
end
|
|
196
259
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# that are visible in deploy.yml alone, so they still run when every host is unreachable.
|
|
3
3
|
class Dash::Cli::Doctor::ConfigChecks
|
|
4
4
|
def run
|
|
5
|
-
readiness_results
|
|
5
|
+
readiness_results + dockerfile_results
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
private
|
|
@@ -25,4 +25,39 @@ class Dash::Cli::Doctor::ConfigChecks
|
|
|
25
25
|
"add a `healthcheck:` block, or opt out with `healthcheck: false`"
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
# The static half of the deploy report's advice: the same rules, without a build to
|
|
30
|
+
# measure against. `advice: false` is not consulted — it silences the block printed
|
|
31
|
+
# next to a deploy, and this check is one the operator asked for by name.
|
|
32
|
+
def dockerfile_results
|
|
33
|
+
dockerfile = DASH.config.builder.dockerfile
|
|
34
|
+
path = File.expand_path(dockerfile)
|
|
35
|
+
|
|
36
|
+
unless File.exist?(path)
|
|
37
|
+
return [ result(:dockerfile, dockerfile, :fail, "not found — `dash build push` fails with Missing #{dockerfile}") ]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
findings = analyzer(dockerfile, path).findings
|
|
41
|
+
return [ result(:dockerfile, dockerfile, :ok, "no findings") ] if findings.empty?
|
|
42
|
+
|
|
43
|
+
findings.map { |finding| finding_result(finding) }
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
[ result(:dockerfile, dockerfile, :warn, "could not be analysed (#{e.class}: #{e.message})") ]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def analyzer(dockerfile, path)
|
|
49
|
+
Dash::Dockerfile::Analyzer.new \
|
|
50
|
+
document: Dash::Dockerfile::Parser.parse(File.read(path)),
|
|
51
|
+
path: dockerfile,
|
|
52
|
+
context_dir: File.expand_path(DASH.config.builder.context),
|
|
53
|
+
builder: DASH.config.builder,
|
|
54
|
+
ignore: DASH.config.report.ignore,
|
|
55
|
+
hadolint: DASH.config.report.hadolint?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# An informational finding is not a reason to hold up a deploy, so it reports as ok
|
|
59
|
+
# with its message intact — visible, but never the difference between ready and not.
|
|
60
|
+
def finding_result(finding)
|
|
61
|
+
result :dockerfile, finding.location, (finding.warn? ? :warn : :ok), "#{finding.message} [#{finding.rule}]"
|
|
62
|
+
end
|
|
28
63
|
end
|
data/lib/dash/cli/doctor.rb
CHANGED
|
@@ -3,13 +3,19 @@ module Dash::Cli::Healthcheck::Poller
|
|
|
3
3
|
|
|
4
4
|
NO_HEALTHCHECK = Dash::Commands::Base::NO_HEALTHCHECK
|
|
5
5
|
|
|
6
|
+
# The wait itself happens on the host now (Dash::Commands::App#wait_for_ready), which
|
|
7
|
+
# returns the moment the status is one this poller accepts and otherwise waits out the
|
|
8
|
+
# deadline it is given. So the block is called once for the wait - and once more only to
|
|
9
|
+
# confirm an unchecked container is still running after its readiness delay. Every
|
|
10
|
+
# decision below is the one the client-side poll made, in the same words; what shrank is
|
|
11
|
+
# the number of round trips it took to reach them.
|
|
6
12
|
def wait_for_healthy(role:, &block)
|
|
7
13
|
attempt = 1
|
|
8
14
|
timeout_at = Time.now + DASH.config.deploy_timeout
|
|
9
15
|
readiness_delay = role.readiness_delay
|
|
10
16
|
|
|
11
17
|
begin
|
|
12
|
-
status = block.call
|
|
18
|
+
status = block.call(:wait, seconds_left(timeout_at))
|
|
13
19
|
|
|
14
20
|
if unchecked?(status)
|
|
15
21
|
ensure_no_healthcheck_drift(role, status)
|
|
@@ -20,7 +26,7 @@ module Dash::Cli::Healthcheck::Poller
|
|
|
20
26
|
# Wait for the readiness delay and confirm it is still running
|
|
21
27
|
if readiness_delay > 0
|
|
22
28
|
sleep readiness_delay
|
|
23
|
-
status = block.call
|
|
29
|
+
status = block.call(:confirm)
|
|
24
30
|
ensure_no_healthcheck_drift(role, status)
|
|
25
31
|
end
|
|
26
32
|
end
|
|
@@ -55,8 +61,14 @@ module Dash::Cli::Healthcheck::Poller
|
|
|
55
61
|
status.to_s.delete_prefix("#{NO_HEALTHCHECK}:")
|
|
56
62
|
end
|
|
57
63
|
|
|
64
|
+
# Shared with the host-side wait, which stops looking on exactly these - see
|
|
65
|
+
# Dash::Commands::Base::READY_STATUSES for why the two have to agree.
|
|
58
66
|
def acceptable?(status)
|
|
59
|
-
|
|
67
|
+
Dash::Commands::Base::READY_STATUSES.include?(status)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def seconds_left(timeout_at)
|
|
71
|
+
[ (timeout_at - Time.now).ceil, 0 ].max
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
# The config asked docker to probe this container and docker is not probing it — the flags
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Turns the server-side readiness wait's progress lines back into the beacon the
|
|
2
|
+
# client-side poll used to print. It is an SSHKit interaction handler, so it sees the
|
|
3
|
+
# wait's stderr as it streams — the operator gets the same once-a-second feedback they
|
|
4
|
+
# got when the laptop was the thing doing the polling, for one round trip instead of one
|
|
5
|
+
# per attempt. The cadence is fixed at a second because the host loop's is.
|
|
6
|
+
#
|
|
7
|
+
# The stream is line-oriented but arrives in chunks (the SSH backend splits on packet
|
|
8
|
+
# boundaries, not newlines), so data is buffered and only whole lines are reported. Only
|
|
9
|
+
# stderr is buffered: the wait's stdout carries the final status, and stdout and stderr are
|
|
10
|
+
# separate SSH streams whose chunks can interleave — folding both into one buffer would let
|
|
11
|
+
# the status land in the middle of a half-arrived progress line and corrupt them both.
|
|
12
|
+
# Anything on stderr that is not a progress line (docker's own complaints) is ignored.
|
|
13
|
+
class Dash::Cli::Healthcheck::ProgressReporter
|
|
14
|
+
LINE = /\A#{Regexp.escape(Dash::Commands::Base::READINESS_PROGRESS_PREFIX)} (?<elapsed>\d+) (?<left>\d+)(?: |\z)/
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@buffer = +""
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# SSHKit's interaction-handler contract.
|
|
22
|
+
def on_data(_command, stream_name, data, _channel = nil)
|
|
23
|
+
return unless stream_name == :stderr
|
|
24
|
+
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
@buffer << data.to_s
|
|
27
|
+
while (newline = @buffer.index("\n"))
|
|
28
|
+
report @buffer.slice!(0..newline).chomp
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
def report(line)
|
|
35
|
+
match = LINE.match(line) or return
|
|
36
|
+
|
|
37
|
+
SSHKit.config.output.info "Container not ready yet, retrying in 1s (#{match[:elapsed]}s elapsed, #{match[:left]}s left)"
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/dash/cli/main.rb
CHANGED
|
@@ -26,16 +26,23 @@ class Dash::Cli::Main < Dash::Cli::Base
|
|
|
26
26
|
print_config_banner
|
|
27
27
|
|
|
28
28
|
say "Validate configuration and secrets...", :magenta
|
|
29
|
-
DASH.config.validate_secrets!(include_accessories: boot_accessories)
|
|
29
|
+
timed("Validate config and secrets") { DASH.config.validate_secrets!(include_accessories: boot_accessories) }
|
|
30
30
|
|
|
31
31
|
if options[:skip_push]
|
|
32
32
|
say "Pull app image...", :magenta
|
|
33
33
|
timed("Pull app image") { invoke "dash:cli:build:pull", [], invoke_options }
|
|
34
34
|
else
|
|
35
35
|
say "Build and push app image...", :magenta
|
|
36
|
-
timed("Build and push app image")
|
|
36
|
+
timed("Build and push app image") do |entry|
|
|
37
|
+
DASH.report.build_entry = entry
|
|
38
|
+
invoke "dash:cli:build:deliver", [], invoke_options
|
|
39
|
+
end
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
# Before the boot, so the advice still prints when a boot fails — a slow build is
|
|
43
|
+
# exactly the kind of thing an operator wants to see on a deploy that went wrong.
|
|
44
|
+
analyze_report
|
|
45
|
+
|
|
39
46
|
modify(lock: true) do
|
|
40
47
|
run_hook "pre-deploy", secrets: true
|
|
41
48
|
|
|
@@ -59,7 +66,7 @@ class Dash::Cli::Main < Dash::Cli::Base
|
|
|
59
66
|
end
|
|
60
67
|
end
|
|
61
68
|
|
|
62
|
-
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
|
|
69
|
+
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s, **report_hook_details
|
|
63
70
|
end
|
|
64
71
|
end
|
|
65
72
|
|
|
@@ -74,16 +81,23 @@ class Dash::Cli::Main < Dash::Cli::Base
|
|
|
74
81
|
print_config_banner
|
|
75
82
|
|
|
76
83
|
say "Validate configuration and secrets...", :magenta
|
|
77
|
-
DASH.config.validate_secrets!
|
|
84
|
+
timed("Validate config and secrets") { DASH.config.validate_secrets! }
|
|
78
85
|
|
|
79
86
|
if options[:skip_push]
|
|
80
87
|
say "Pull app image...", :magenta
|
|
81
88
|
timed("Pull app image") { invoke "dash:cli:build:pull", [], invoke_options }
|
|
82
89
|
else
|
|
83
90
|
say "Build and push app image...", :magenta
|
|
84
|
-
timed("Build and push app image")
|
|
91
|
+
timed("Build and push app image") do |entry|
|
|
92
|
+
DASH.report.build_entry = entry
|
|
93
|
+
invoke "dash:cli:build:deliver", [], invoke_options
|
|
94
|
+
end
|
|
85
95
|
end
|
|
86
96
|
|
|
97
|
+
# Before the boot, so the advice still prints when a boot fails — a slow build is
|
|
98
|
+
# exactly the kind of thing an operator wants to see on a deploy that went wrong.
|
|
99
|
+
analyze_report
|
|
100
|
+
|
|
87
101
|
modify(lock: true) do
|
|
88
102
|
run_hook "pre-deploy", secrets: true
|
|
89
103
|
|
|
@@ -99,7 +113,7 @@ class Dash::Cli::Main < Dash::Cli::Base
|
|
|
99
113
|
end
|
|
100
114
|
end
|
|
101
115
|
|
|
102
|
-
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
|
|
116
|
+
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s, **report_hook_details
|
|
103
117
|
end
|
|
104
118
|
end
|
|
105
119
|
|
|
@@ -125,7 +139,7 @@ class Dash::Cli::Main < Dash::Cli::Base
|
|
|
125
139
|
end
|
|
126
140
|
end
|
|
127
141
|
|
|
128
|
-
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
|
|
142
|
+
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s, **report_hook_details if rolled_back
|
|
129
143
|
end
|
|
130
144
|
end
|
|
131
145
|
|
|
@@ -299,6 +313,9 @@ class Dash::Cli::Main < Dash::Cli::Base
|
|
|
299
313
|
desc "prune", "Prune old application images and containers"
|
|
300
314
|
subcommand "prune", Dash::Cli::Prune
|
|
301
315
|
|
|
316
|
+
desc "report", "Read the deploy reports saved under .dash/reports"
|
|
317
|
+
subcommand "report", Dash::Cli::Report
|
|
318
|
+
|
|
302
319
|
desc "registry", "Login and -out of the image registry"
|
|
303
320
|
subcommand "registry", Dash::Cli::Registry
|
|
304
321
|
|
data/lib/dash/cli/proxy/drift.rb
CHANGED
|
@@ -7,8 +7,23 @@ class Dash::Cli::Proxy::Drift
|
|
|
7
7
|
@sshkit = sshkit
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
# One `docker inspect` for everything a boot asks about the running proxy: whether it
|
|
11
|
+
# exists, which image tag it runs, and the digest it was booted with. Captured once per
|
|
12
|
+
# instance - `dash proxy boot` reads all three off it, and `dash doctor` only the first.
|
|
13
|
+
def state
|
|
14
|
+
@state ||= Dash::Commands::Proxy::State.parse(
|
|
15
|
+
capture_with_info(*proxy.inspect_state, raise_on_non_zero_exit: false)
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
10
19
|
def container_exists?
|
|
11
|
-
|
|
20
|
+
state.exists?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The tag the running proxy was booted from, for the minimum-version gate. Nil when
|
|
24
|
+
# nothing is running - a host with no proxy has no version to be too old.
|
|
25
|
+
def version
|
|
26
|
+
state.version
|
|
12
27
|
end
|
|
13
28
|
|
|
14
29
|
# A proxy container has drifted when it was started with a different config
|
|
@@ -30,7 +45,7 @@ class Dash::Cli::Proxy::Drift
|
|
|
30
45
|
|
|
31
46
|
private
|
|
32
47
|
def current_digest
|
|
33
|
-
|
|
48
|
+
state.digest.to_s
|
|
34
49
|
end
|
|
35
50
|
|
|
36
51
|
def proxy
|
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
# so a second deploy is a no-op. Nothing here removes the legacy network or
|
|
24
24
|
# volume: an operator who wants them gone removes them by hand, and stage 3d
|
|
25
25
|
# deletes this class outright.
|
|
26
|
+
#
|
|
27
|
+
# All three travel as one command (Dash::Commands::Proxy#prepare_boot), guarded on a
|
|
28
|
+
# marker the host writes once it is verifiably past the rename - so a migrated host, and
|
|
29
|
+
# a host installed fresh on 4.x that never had a kamal-proxy, run no docker command here
|
|
30
|
+
# at all. The round trip itself is one the host already pays: the command carries the
|
|
31
|
+
# apps-config `mkdir -p` too, which reads nothing the bridge writes. Stage 3d keeps the
|
|
32
|
+
# mkdir and deletes the rest.
|
|
26
33
|
class Dash::Cli::Proxy::LegacyRename
|
|
27
34
|
attr_reader :host, :sshkit
|
|
28
35
|
delegate :execute, to: :sshkit
|
|
@@ -33,26 +40,6 @@ class Dash::Cli::Proxy::LegacyRename
|
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def run
|
|
36
|
-
|
|
37
|
-
adopt_config_volume
|
|
38
|
-
replace_legacy_container
|
|
43
|
+
execute *DASH.proxy(host).prepare_boot
|
|
39
44
|
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
def bridge_network
|
|
43
|
-
execute *DASH.docker.connect_legacy_network_containers
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def adopt_config_volume
|
|
47
|
-
execute *DASH.proxy(host).copy_legacy_config_volume
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# The drain timeout the proxy is configured with, so a busy host is not cut
|
|
51
|
-
# off mid-request any more abruptly than a normal reboot would.
|
|
52
|
-
def replace_legacy_container
|
|
53
|
-
proxy = DASH.proxy(host)
|
|
54
|
-
|
|
55
|
-
execute *proxy.remove_legacy_container(timeout: DASH.config.drain_timeout)
|
|
56
|
-
execute *proxy.remove_legacy_holder_container
|
|
57
|
-
end
|
|
58
45
|
end
|
|
@@ -21,6 +21,14 @@ class Dash::Cli::Proxy::LoadbalancerReboot
|
|
|
21
21
|
execute *DASH.registry.login
|
|
22
22
|
ensure_network
|
|
23
23
|
|
|
24
|
+
# After the network the bridge attaches the legacy network's containers to,
|
|
25
|
+
# and before anything that could create the new container or let `docker
|
|
26
|
+
# run --volume` create dash-loadbalancer-config empty: adopt the legacy
|
|
27
|
+
# volume's routing table, dynamic domains and ACME cache. Carries the
|
|
28
|
+
# apps-config mkdir this reboot paid a round trip for anyway
|
|
29
|
+
# (zoolutions/dash#168, see Dash::Commands::Loadbalancer#legacy_rename).
|
|
30
|
+
execute *DASH.loadbalancer.prepare_boot
|
|
31
|
+
|
|
24
32
|
info "Stopping and removing #{DASH.loadbalancer.container_name} on #{host}, if running..."
|
|
25
33
|
execute *DASH.loadbalancer.stop, raise_on_non_zero_exit: false
|
|
26
34
|
execute *DASH.loadbalancer.remove_container
|
|
@@ -32,7 +40,6 @@ class Dash::Cli::Proxy::LoadbalancerReboot
|
|
|
32
40
|
execute *DASH.loadbalancer.remove_proxy_secrets_file, raise_on_non_zero_exit: false
|
|
33
41
|
end
|
|
34
42
|
|
|
35
|
-
execute *DASH.loadbalancer.ensure_apps_config_directory
|
|
36
43
|
Dash::Cli::Proxy::LoadbalancerClaim.new(host, sshkit).claim_run_config(replace: true)
|
|
37
44
|
execute *DASH.loadbalancer.run
|
|
38
45
|
|
|
@@ -37,7 +37,12 @@ class Dash::Cli::Proxy::Reboot
|
|
|
37
37
|
|
|
38
38
|
def replace_container
|
|
39
39
|
execute *proxy.ensure_proxy_directory
|
|
40
|
-
|
|
40
|
+
# Before the new container - and the new config volume `docker run
|
|
41
|
+
# --volume` would otherwise create empty - exists: bring a host still on
|
|
42
|
+
# pre-rename identity across. Carries the apps-config mkdir this reboot
|
|
43
|
+
# paid a round trip for anyway, so the bridge costs none here either
|
|
44
|
+
# (zoolutions/dash#168, see Dash::Cli::Proxy::LegacyRename).
|
|
45
|
+
execute *proxy.prepare_boot
|
|
41
46
|
sync_proxy_secrets
|
|
42
47
|
|
|
43
48
|
if proxy.port_holder?
|
data/lib/dash/cli/proxy.rb
CHANGED
|
@@ -22,7 +22,9 @@ class Dash::Cli::Proxy < Dash::Cli::Base
|
|
|
22
22
|
execute *DASH.registry.login
|
|
23
23
|
|
|
24
24
|
# Before anything reads the new container, volume or network: bring a
|
|
25
|
-
# host still on pre-rename identity across
|
|
25
|
+
# host still on pre-rename identity across, and make the apps-config
|
|
26
|
+
# directory in the same round trip. Nothing but a `test -f` once it has
|
|
27
|
+
# been - see Dash::Cli::Proxy::LegacyRename.
|
|
26
28
|
Dash::Cli::Proxy::LegacyRename.new(host, self).run
|
|
27
29
|
|
|
28
30
|
proxy = DASH.proxy(host)
|
|
@@ -34,7 +36,8 @@ class Dash::Cli::Proxy < Dash::Cli::Base
|
|
|
34
36
|
else
|
|
35
37
|
stale_hosts << host.to_s if drift.drifted?
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
# The tag off the inspect the drift check already made, not a read of its own.
|
|
40
|
+
version = drift.version
|
|
38
41
|
|
|
39
42
|
if version && Dash::Utils.older_version?(version, Dash::Configuration::Proxy::Run::MINIMUM_VERSION)
|
|
40
43
|
raise "dash-proxy version #{version} is too old, run `dash proxy reboot` in order to update to at least #{Dash::Configuration::Proxy::Run::MINIMUM_VERSION}"
|
|
@@ -48,7 +51,6 @@ class Dash::Cli::Proxy < Dash::Cli::Base
|
|
|
48
51
|
execute *proxy.remove_proxy_secrets_file, raise_on_non_zero_exit: false
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
execute *proxy.ensure_apps_config_directory
|
|
52
54
|
execute *proxy.start_holder_or_run if proxy.port_holder?
|
|
53
55
|
execute *proxy.start_or_run(digest: drift.expected_digest)
|
|
54
56
|
end
|
|
@@ -77,10 +79,10 @@ class Dash::Cli::Proxy < Dash::Cli::Base
|
|
|
77
79
|
execute *DASH.registry.login
|
|
78
80
|
|
|
79
81
|
# Bring a pre-rename host across before the container can be created
|
|
80
|
-
# against an empty volume or a network nothing else joined
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
execute *DASH.loadbalancer.
|
|
82
|
+
# against an empty volume or a network nothing else joined, and make
|
|
83
|
+
# the apps-config directory in the same round trip. Nothing but a
|
|
84
|
+
# `test -f` once it has been.
|
|
85
|
+
execute *DASH.loadbalancer.prepare_boot
|
|
84
86
|
|
|
85
87
|
# The load balancer terminates TLS and owns the cache, so its host
|
|
86
88
|
# needs the proxy secrets (acme credentials, cache store) just like
|
|
@@ -92,8 +94,6 @@ class Dash::Cli::Proxy < Dash::Cli::Base
|
|
|
92
94
|
execute *DASH.loadbalancer.remove_proxy_secrets_file, raise_on_non_zero_exit: false
|
|
93
95
|
end
|
|
94
96
|
|
|
95
|
-
execute *DASH.loadbalancer.ensure_apps_config_directory
|
|
96
|
-
|
|
97
97
|
# TLS terminates at the load balancer, so the TLS material the app
|
|
98
98
|
# hosts get - custom certificates and the mTLS client CA - must
|
|
99
99
|
# reach this host too; the LB container reads it through the same
|
|
@@ -105,10 +105,11 @@ class Dash::Cli::Proxy < Dash::Cli::Base
|
|
|
105
105
|
# The same drift detection the proxy hosts get: a loadbalancer booted
|
|
106
106
|
# with a different config digest reboots below (or warns, when
|
|
107
107
|
# automatic reboot is off) instead of serving a stale config forever.
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
state = Dash::Commands::Proxy::State.parse(
|
|
109
|
+
capture_with_info(*DASH.loadbalancer.inspect_state, raise_on_non_zero_exit: false)
|
|
110
|
+
)
|
|
110
111
|
|
|
111
|
-
if
|
|
112
|
+
if state.exists? && state.digest.to_s != DASH.loadbalancer_config.run_config_digest
|
|
112
113
|
if auto_reboot
|
|
113
114
|
# Leave the old loadbalancer serving until its reboot below.
|
|
114
115
|
lb_drifted << host.to_s
|
|
@@ -413,6 +414,12 @@ class Dash::Cli::Proxy < Dash::Cli::Base
|
|
|
413
414
|
if DASH.config.proxy.load_balancing?
|
|
414
415
|
on(DASH.config.proxy.effective_loadbalancer) do |host|
|
|
415
416
|
execute *DASH.registry.login
|
|
417
|
+
# start_or_run falls through to `docker run` on a host with no
|
|
418
|
+
# container, so this is a volume-creating path too and gets the same
|
|
419
|
+
# bridge boot and reboot do (zoolutions/dash#168). It also makes the
|
|
420
|
+
# apps-config directory the bind mount would otherwise have docker
|
|
421
|
+
# create root-owned.
|
|
422
|
+
execute *DASH.loadbalancer.prepare_boot
|
|
416
423
|
execute *DASH.loadbalancer.start_or_run
|
|
417
424
|
end
|
|
418
425
|
else
|
data/lib/dash/cli/prune.rb
CHANGED
|
@@ -11,9 +11,7 @@ class Dash::Cli::Prune < Dash::Cli::Base
|
|
|
11
11
|
def images
|
|
12
12
|
modify(lock: true, server_lock: true) do
|
|
13
13
|
on(DASH.hosts) do
|
|
14
|
-
execute *DASH.auditor.
|
|
15
|
-
execute *DASH.prune.dangling_images
|
|
16
|
-
execute *DASH.prune.tagged_images
|
|
14
|
+
execute *DASH.auditor.record_then("Pruned images", DASH.prune.dangling_images, DASH.prune.tagged_images)
|
|
17
15
|
end
|
|
18
16
|
end
|
|
19
17
|
end
|
|
@@ -26,11 +24,10 @@ class Dash::Cli::Prune < Dash::Cli::Base
|
|
|
26
24
|
|
|
27
25
|
modify(lock: true, server_lock: true) do
|
|
28
26
|
on(DASH.hosts) do |host|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
DASH.
|
|
32
|
-
|
|
33
|
-
end
|
|
27
|
+
# One round trip per host, whatever it runs: a host with no app roles still
|
|
28
|
+
# records that the sweep reached it.
|
|
29
|
+
execute *DASH.auditor.record_then("Pruned containers",
|
|
30
|
+
*DASH.roles_on(host).map { |role| DASH.prune.app_containers(retain: retain, role: role) })
|
|
34
31
|
end
|
|
35
32
|
end
|
|
36
33
|
end
|