dash 4.0.8 → 4.1.0
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 +20 -10
- data/lib/dash/cli/app.rb +2 -2
- data/lib/dash/cli/base.rb +117 -2
- data/lib/dash/cli/build.rb +61 -5
- data/lib/dash/cli/doctor/config_checks.rb +36 -1
- data/lib/dash/cli/doctor.rb +2 -1
- data/lib/dash/cli/main.rb +24 -7
- 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 +18 -0
- data/lib/dash/commands/auditor.rb +10 -0
- data/lib/dash/commands/builder/base.rb +18 -0
- data/lib/dash/commands/builder.rb +1 -1
- data/lib/dash/configuration/docs/configuration.yml +6 -0
- data/lib/dash/configuration/docs/report.yml +39 -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 +36 -1
data/lib/dash/cli/doctor.rb
CHANGED
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/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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
# Reads the JSON reports every deploy leaves under `.dash/reports`.
|
|
4
|
+
#
|
|
5
|
+
# Entirely local and read-only: no lock, no SSH, nothing that can change a server. It is
|
|
6
|
+
# the command to reach for after a deploy has finished and the table has scrolled away,
|
|
7
|
+
# and the one that answers "was it always this slow?".
|
|
8
|
+
class Dash::Cli::Report < Dash::Cli::Base
|
|
9
|
+
default_command :show
|
|
10
|
+
|
|
11
|
+
TREND_HEADINGS = %w[ started version total build boot advice ].freeze
|
|
12
|
+
COLUMNS = " %-20s %-10s %8s %8s %8s %s".freeze
|
|
13
|
+
|
|
14
|
+
desc "show", "Print the last saved deploy report"
|
|
15
|
+
option :last, type: :numeric, banner: "N", desc: "Print a trend table over the last N reports instead"
|
|
16
|
+
def show
|
|
17
|
+
if (last = options[:last])
|
|
18
|
+
return say "--last takes a positive number of reports, got #{last}", :red unless count?(last)
|
|
19
|
+
|
|
20
|
+
print_trend saved.recent(last.to_i)
|
|
21
|
+
else
|
|
22
|
+
print_latest saved.recent(1).first
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc "path", "Print the directory saved reports are written to"
|
|
27
|
+
def path
|
|
28
|
+
puts reports_directory
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
# Thor's :numeric happily hands over -1 or 2.5, which Array#first turns into a
|
|
33
|
+
# backtrace. A typo in a flag deserves a sentence, not a stack trace.
|
|
34
|
+
def count?(value)
|
|
35
|
+
value.to_i == value && value.to_i > 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def saved
|
|
39
|
+
Dash::Report::History.new(reports_directory, destination: DASH.config.destination)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def print_latest(document)
|
|
43
|
+
return say_nothing_saved unless document
|
|
44
|
+
|
|
45
|
+
say "Deploy report for #{subject}", :magenta
|
|
46
|
+
puts summary_line(document)
|
|
47
|
+
puts Dash::Report.from_h(document).lines
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def print_trend(documents)
|
|
51
|
+
return say_nothing_saved if documents.empty?
|
|
52
|
+
|
|
53
|
+
say "Last #{documents.size} #{"report".pluralize(documents.size)} for #{subject}", :magenta
|
|
54
|
+
puts format(COLUMNS, *TREND_HEADINGS)
|
|
55
|
+
documents.reverse_each { |document| puts trend_row(document) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def subject
|
|
59
|
+
[ DASH.config.service, ("to #{DASH.config.destination}" if DASH.config.destination) ].compact.join(" ")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def summary_line(document)
|
|
63
|
+
" #{document[:command]} #{document[:status]} in #{seconds(document[:runtime])} at #{document[:started_at]}" \
|
|
64
|
+
"#{" (version #{document[:version]})" if document[:version]}#{error_note(document)}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def error_note(document)
|
|
68
|
+
" — #{document.dig(:error, :class)}: #{document.dig(:error, :message)}" if document[:error]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def trend_row(document)
|
|
72
|
+
format COLUMNS, document[:started_at], (document[:version] || "").to_s[0...10],
|
|
73
|
+
seconds(document[:runtime]), phase(document, Dash::Report::Trends::BUILD_PHASE),
|
|
74
|
+
phase(document, Dash::Report::Trends::BOOT_PHASE), advice_count(document)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def phase(document, name)
|
|
78
|
+
found = Array(document[:phases]).find { |candidate| candidate[:name] == name && candidate[:depth].to_i.zero? }
|
|
79
|
+
|
|
80
|
+
found ? seconds(found[:seconds]) : "-"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def advice_count(document)
|
|
84
|
+
findings = Array(document[:advice])
|
|
85
|
+
warnings = findings.count { |finding| finding[:severity] == "warn" }
|
|
86
|
+
|
|
87
|
+
"#{findings.size}#{" (#{warnings} warn)" if warnings > 0}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def seconds(value)
|
|
91
|
+
format("%.1fs", value.to_f)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def say_nothing_saved
|
|
95
|
+
say "No saved reports for #{subject} in #{reports_directory}", :yellow
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -10,5 +10,10 @@
|
|
|
10
10
|
# DASH_ROLES (if set)
|
|
11
11
|
# DASH_DESTINATION (if set)
|
|
12
12
|
# DASH_RUNTIME
|
|
13
|
+
# DASH_BUILD_RUNTIME (if the deploy built an image)
|
|
14
|
+
# DASH_BOOT_RUNTIME (if the deploy booted containers)
|
|
15
|
+
# DASH_ADVICE_COUNT
|
|
16
|
+
# DASH_ADVICE_WARNINGS
|
|
17
|
+
# DASH_REPORT_PATH (unless report/history is 0)
|
|
13
18
|
|
|
14
19
|
echo "$DASH_PERFORMER deployed $DASH_VERSION to $DASH_DESTINATION in $DASH_RUNTIME seconds"
|
data/lib/dash/commander.rb
CHANGED
|
@@ -6,7 +6,13 @@ require "active_support/notifications"
|
|
|
6
6
|
|
|
7
7
|
class Dash::Commander
|
|
8
8
|
attr_accessor :verbosity, :holding_lock, :holding_server_lock, :connected, :logging, :lock_wait, :lock_wait_timeout, :lock_wait_interval
|
|
9
|
-
attr_reader :specific_roles, :specific_hosts, :timings
|
|
9
|
+
attr_reader :specific_roles, :specific_hosts, :timings, :report
|
|
10
|
+
|
|
11
|
+
# Hosts whose run directory this process has already swept, so the second lock acquire
|
|
12
|
+
# of a command does not re-run the migration everywhere. Per host rather than a flag:
|
|
13
|
+
# `dash upgrade` narrows the host set between acquires, and a host that was never in
|
|
14
|
+
# scope has never been swept.
|
|
15
|
+
attr_reader :run_directory_ensured_on
|
|
10
16
|
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
|
|
11
17
|
|
|
12
18
|
def initialize
|
|
@@ -24,10 +30,12 @@ class Dash::Commander
|
|
|
24
30
|
self.lock_wait_interval = 15
|
|
25
31
|
@modify_depth = 0
|
|
26
32
|
@timings = Dash::Timings.new
|
|
33
|
+
@report = Dash::Report.new(timings: @timings)
|
|
27
34
|
@specifics = @specific_roles = @specific_hosts = nil
|
|
28
35
|
@config = @config_kwargs = nil
|
|
29
36
|
@output_logger = nil
|
|
30
37
|
@commands = {}
|
|
38
|
+
@run_directory_ensured_on = []
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def config
|
|
@@ -170,7 +178,7 @@ class Dash::Commander
|
|
|
170
178
|
@logging = true
|
|
171
179
|
if modify_started
|
|
172
180
|
ActiveSupport::Notifications.instrument("modify.kamal",
|
|
173
|
-
command: command, subcommand: subcommand, destination: config.destination, hosts: hosts) { yield }
|
|
181
|
+
command: command, subcommand: subcommand, destination: config.destination, hosts: hosts, report: report) { yield }
|
|
174
182
|
else
|
|
175
183
|
yield
|
|
176
184
|
end
|
data/lib/dash/commands/app.rb
CHANGED
|
@@ -3,6 +3,10 @@ class Dash::Commands::App < Dash::Commands::Base
|
|
|
3
3
|
|
|
4
4
|
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
|
5
5
|
|
|
6
|
+
# Separates the two answers #boot_state returns. A container id is hex and a version is
|
|
7
|
+
# a name suffix, so neither can produce this line on its own.
|
|
8
|
+
BOOT_STATE_SEPARATOR = "--%--"
|
|
9
|
+
|
|
6
10
|
attr_reader :role, :host
|
|
7
11
|
|
|
8
12
|
delegate :container_name, to: :role
|
|
@@ -75,6 +79,20 @@ class Dash::Commands::App < Dash::Commands::Base
|
|
|
75
79
|
extract_version_from_name
|
|
76
80
|
end
|
|
77
81
|
|
|
82
|
+
# Everything a boot needs to know about a host before it starts anything: whether a
|
|
83
|
+
# container for the version being deployed already exists (so it can be renamed out of
|
|
84
|
+
# the way) and which version is running now (so it can be stopped once the new one is
|
|
85
|
+
# live). Two questions, one round trip, answers split on BOOT_STATE_SEPARATOR.
|
|
86
|
+
#
|
|
87
|
+
# Chained with `;` rather than `&&`: an empty answer to either is a normal result, not
|
|
88
|
+
# a failure, and the second question must be asked whatever the first one said.
|
|
89
|
+
def boot_state(version)
|
|
90
|
+
chain \
|
|
91
|
+
container_id_for_version(version),
|
|
92
|
+
[ :echo, BOOT_STATE_SEPARATOR ],
|
|
93
|
+
current_running_version
|
|
94
|
+
end
|
|
95
|
+
|
|
78
96
|
def list_versions(*docker_args, statuses: nil)
|
|
79
97
|
pipe \
|
|
80
98
|
docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
|
@@ -14,6 +14,16 @@ class Dash::Commands::Auditor < Dash::Commands::Base
|
|
|
14
14
|
append([ :echo, escape_shell_value(audit_line(line, **details)) ], audit_log_file)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# The audit line and the action it describes in one round trip, still in that order:
|
|
18
|
+
# the log is written first, and `&&` means a failed write aborts the action exactly as
|
|
19
|
+
# a failed standalone audit would have.
|
|
20
|
+
#
|
|
21
|
+
# Only ever fold in commands the caller would `execute`. A `capture` folded in here
|
|
22
|
+
# would come back with nothing to distinguish the audit's own output from the answer.
|
|
23
|
+
def record_then(line, *commands, **details)
|
|
24
|
+
combine record(line, **details), *commands
|
|
25
|
+
end
|
|
26
|
+
|
|
17
27
|
def reveal
|
|
18
28
|
[ :tail, "-n", 50, audit_log_file ]
|
|
19
29
|
end
|
|
@@ -14,9 +14,27 @@ class Dash::Commands::Builder::Base < Dash::Commands::Base
|
|
|
14
14
|
docker :image, :rm, "--force", config.absolute_image
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Dropping the old image is housekeeping - a host that never had it is not an error -
|
|
18
|
+
# so it must not short-circuit whatever it shares a round trip with.
|
|
19
|
+
#
|
|
20
|
+
# The `|| true` is parenthesised because `&&` and `||` bind equally and associate left:
|
|
21
|
+
# ungrouped, an `audit && clean || true && pull` chain lets a FAILED audit fall into the
|
|
22
|
+
# same `|| true` and pull anyway, exit status 0. The group confines it to the clean.
|
|
23
|
+
#
|
|
24
|
+
# Composed only, never executed on its own: SSHKit's command map prefixes an unknown
|
|
25
|
+
# first word with /usr/bin/env, and the first word here is `(`.
|
|
26
|
+
def clean_then_pull
|
|
27
|
+
combine [ "(", *any(clean, [ :true ]), ")" ], pull
|
|
28
|
+
end
|
|
29
|
+
|
|
17
30
|
def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
|
|
18
31
|
docker :buildx, :build,
|
|
19
32
|
"--output=type=#{export_action}",
|
|
33
|
+
# Plain progress is what dash parses into the build rows of the deploy report
|
|
34
|
+
# (Dash::Build::ProgressParser). buildx already falls back to it when stdout is a
|
|
35
|
+
# pipe, which it always is under SSHKit — this only pins it so the format cannot
|
|
36
|
+
# change under us.
|
|
37
|
+
"--progress=plain",
|
|
20
38
|
*platform_options(arches),
|
|
21
39
|
*([ "--builder", builder_name ] unless docker_driver?),
|
|
22
40
|
*build_tag_options(tag_as_dirty: tag_as_dirty),
|
|
@@ -2,7 +2,7 @@ require "active_support/core_ext/string/filters"
|
|
|
2
2
|
|
|
3
3
|
class Dash::Commands::Builder < Dash::Commands::Base
|
|
4
4
|
delegate \
|
|
5
|
-
:create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder,
|
|
5
|
+
:create, :remove, :dev, :push, :clean, :pull, :clean_then_pull, :info, :inspect_builder,
|
|
6
6
|
:validate_image, :first_mirror, :login_to_registry_locally?, :push_env,
|
|
7
7
|
to: :target
|
|
8
8
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Deploy report
|
|
2
|
+
#
|
|
3
|
+
# Every deploy, redeploy, setup, rollback and standalone build prints a report: a table of
|
|
4
|
+
# where the time went, the build steps underneath it, and advice about the Dockerfile and
|
|
5
|
+
# the build context. The `report` key controls the advice half of that; the table always
|
|
6
|
+
# prints.
|
|
7
|
+
|
|
8
|
+
# Report options
|
|
9
|
+
#
|
|
10
|
+
# The options are specified under the report key in the configuration file.
|
|
11
|
+
report:
|
|
12
|
+
|
|
13
|
+
# Advice
|
|
14
|
+
#
|
|
15
|
+
# Print the Advice block under the table. The measurements themselves are always
|
|
16
|
+
# collected — this only decides whether dash says anything about them.
|
|
17
|
+
advice: true
|
|
18
|
+
|
|
19
|
+
# hadolint
|
|
20
|
+
#
|
|
21
|
+
# Supplement dash's own rules with hadolint's when it is installed.
|
|
22
|
+
#
|
|
23
|
+
# `auto` runs it when `hadolint` is on PATH and stays silent when it is not; `false`
|
|
24
|
+
# never runs it. Any other value is a configuration error rather than a silent off.
|
|
25
|
+
# hadolint's exit status never affects the deploy, and its rule codes (DL3008 and
|
|
26
|
+
# friends) can be silenced through `ignore` like any other rule.
|
|
27
|
+
hadolint: auto
|
|
28
|
+
|
|
29
|
+
# History
|
|
30
|
+
#
|
|
31
|
+
# How many JSON reports to keep per destination under .dash/reports. Set to 0 to write
|
|
32
|
+
# none.
|
|
33
|
+
history: 20
|
|
34
|
+
|
|
35
|
+
# Ignore
|
|
36
|
+
#
|
|
37
|
+
# Rule ids to silence. The id is the string printed with each piece of advice.
|
|
38
|
+
ignore:
|
|
39
|
+
- root-user
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# The `report:` block: how much of the deploy report to print, and how much of it to keep.
|
|
2
|
+
#
|
|
3
|
+
# Every key is optional and the defaults are what an operator who has never heard of the
|
|
4
|
+
# block gets — the table always prints, the advice under it prints, and hadolint joins in
|
|
5
|
+
# only if they already have it installed.
|
|
6
|
+
class Dash::Configuration::Report
|
|
7
|
+
include Dash::Configuration::Validation
|
|
8
|
+
|
|
9
|
+
DEFAULT_HISTORY = 20
|
|
10
|
+
HADOLINT_AUTO = "auto".freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :report_config
|
|
13
|
+
|
|
14
|
+
HADOLINT_SETTINGS = [ HADOLINT_AUTO, true, false ].freeze
|
|
15
|
+
|
|
16
|
+
def initialize(config:)
|
|
17
|
+
@report_config = config.raw_config.report || {}
|
|
18
|
+
validate! @report_config unless @report_config.empty?
|
|
19
|
+
ensure_valid_hadolint_setting
|
|
20
|
+
ensure_valid_history
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def advice?
|
|
24
|
+
report_config.fetch("advice", true)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def hadolint
|
|
28
|
+
report_config.fetch("hadolint", HADOLINT_AUTO)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# "auto" (or true) means run it when it is on PATH — the availability check itself
|
|
32
|
+
# lives in Dash::Dockerfile::Hadolint, because only it knows what running costs.
|
|
33
|
+
def hadolint?
|
|
34
|
+
hadolint != false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def history
|
|
38
|
+
report_config.fetch("history", DEFAULT_HISTORY).to_i
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ignore
|
|
42
|
+
Array(report_config["ignore"]).map(&:to_s)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
report_config
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
# A misspelling must not read as "off": the operator would lose findings and never
|
|
51
|
+
# learn why.
|
|
52
|
+
def ensure_valid_hadolint_setting
|
|
53
|
+
return if HADOLINT_SETTINGS.include?(hadolint)
|
|
54
|
+
|
|
55
|
+
raise Dash::ConfigurationError, "report/hadolint: must be auto or false, got #{hadolint.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Same reasoning: a negative count is a typo, and reading it as "keep none" would
|
|
59
|
+
# quietly stop saving the reports the operator was configuring.
|
|
60
|
+
def ensure_valid_history
|
|
61
|
+
return if history >= 0
|
|
62
|
+
|
|
63
|
+
raise Dash::ConfigurationError, "report/history: must be 0 or more, got #{report_config["history"].inspect}"
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/dash/configuration.rb
CHANGED
|
@@ -16,7 +16,7 @@ class Dash::Configuration
|
|
|
16
16
|
delegate :argumentize, :optionize, to: Dash::Utils
|
|
17
17
|
|
|
18
18
|
attr_reader :destination, :raw_config, :secrets
|
|
19
|
-
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :output, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry
|
|
19
|
+
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :output, :proxy, :proxy_boot, :report, :servers, :ssh, :sshkit, :registry
|
|
20
20
|
|
|
21
21
|
include Validation
|
|
22
22
|
|
|
@@ -76,6 +76,7 @@ class Dash::Configuration
|
|
|
76
76
|
|
|
77
77
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
|
78
78
|
@output = Output.new(config: self)
|
|
79
|
+
@report = Report.new(config: self)
|
|
79
80
|
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
|
|
80
81
|
@proxy_boot = Proxy::Boot.new(config: self)
|
|
81
82
|
@ssh = Ssh.new(config: self)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Runs the rule set over a parsed Dockerfile and returns the advice.
|
|
2
|
+
#
|
|
3
|
+
# Rules that need measurements (`build:`) stay silent without them, so the same analyzer
|
|
4
|
+
# serves `dash doctor` — static, no build, no SSH — and the block printed under a deploy,
|
|
5
|
+
# where the numbers upgrade a static hint into "this cost you 84.1 seconds".
|
|
6
|
+
class Dash::Dockerfile::Analyzer
|
|
7
|
+
RULES = [
|
|
8
|
+
Dash::Dockerfile::Rules::CopyBeforeInstall,
|
|
9
|
+
Dash::Dockerfile::Rules::ContextSize,
|
|
10
|
+
Dash::Dockerfile::Rules::MissingDockerignore,
|
|
11
|
+
Dash::Dockerfile::Rules::DockerignoreGaps,
|
|
12
|
+
Dash::Dockerfile::Rules::LatestBase,
|
|
13
|
+
Dash::Dockerfile::Rules::SecretInBuildArg,
|
|
14
|
+
Dash::Dockerfile::Rules::SingleStageBuildDeps,
|
|
15
|
+
Dash::Dockerfile::Rules::AptHygiene,
|
|
16
|
+
Dash::Dockerfile::Rules::CacheBustingArg,
|
|
17
|
+
Dash::Dockerfile::Rules::CacheExportCost,
|
|
18
|
+
Dash::Dockerfile::Rules::CurlPipeShell,
|
|
19
|
+
Dash::Dockerfile::Rules::InlineEnvBlob,
|
|
20
|
+
Dash::Dockerfile::Rules::NoCacheMount,
|
|
21
|
+
Dash::Dockerfile::Rules::UncachedInstall,
|
|
22
|
+
Dash::Dockerfile::Rules::RootUser
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
# Reads and parses the file. Missing is the caller's problem to report: `dash doctor`
|
|
26
|
+
# fails the check, a deploy stays quiet (a --skip-push deploy has no Dockerfile and no
|
|
27
|
+
# business complaining about it).
|
|
28
|
+
def self.for_file(path, **options)
|
|
29
|
+
new document: Dash::Dockerfile::Parser.parse(File.read(path)), path: path, file: path, **options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# `path` is what findings print (the operator's own `builder: dockerfile:`); `file` is
|
|
33
|
+
# where the file actually is, for the one rule that has to open it again.
|
|
34
|
+
def initialize(document:, context_dir: nil, build: nil, builder: nil, path: "Dockerfile", file: path, ignore: [], hadolint: false)
|
|
35
|
+
# A context that is not a local directory (a git URL, or a clone that has not been
|
|
36
|
+
# prepared yet) is not something the .dockerignore rules can say anything about.
|
|
37
|
+
directory = context_dir if context_dir && File.directory?(context_dir)
|
|
38
|
+
|
|
39
|
+
@context = Dash::Dockerfile::Context.new \
|
|
40
|
+
document: document, context_dir: directory, dockerignore: (Dash::Dockerfile::Dockerignore.in(directory) if directory),
|
|
41
|
+
build: build, builder: builder, path: path
|
|
42
|
+
@file = file
|
|
43
|
+
@ignore = Array(ignore).map(&:to_s)
|
|
44
|
+
@hadolint = hadolint
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Warnings first, then the informational findings, each group in rule order — an
|
|
48
|
+
# operator reading the block top down sees what is costing them before what is merely
|
|
49
|
+
# worth knowing.
|
|
50
|
+
def findings
|
|
51
|
+
(rule_findings + hadolint_findings)
|
|
52
|
+
.reject { |finding| @ignore.include?(finding.rule) }
|
|
53
|
+
.sort_by.with_index { |finding, index| [ finding.warn? ? 0 : 1, index ] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
def rule_findings
|
|
58
|
+
RULES.flat_map { |rule| rule.new(@context).findings }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hadolint_findings
|
|
62
|
+
return [] unless @hadolint
|
|
63
|
+
|
|
64
|
+
Dash::Dockerfile::Hadolint.new(path: @context.path, file: @file).findings
|
|
65
|
+
end
|
|
66
|
+
end
|