dash 4.1.3 → 4.2.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/autoscale/connections/builder.rb +51 -0
- data/lib/dash/autoscale/connections.rb +102 -0
- data/lib/dash/autoscale/controller.rb +296 -0
- data/lib/dash/autoscale/cron.rb +75 -0
- data/lib/dash/autoscale/decision.rb +89 -0
- data/lib/dash/autoscale/duration.rb +19 -0
- data/lib/dash/autoscale/evaluation.rb +52 -0
- data/lib/dash/autoscale/guard.rb +48 -0
- data/lib/dash/autoscale/lease.rb +55 -0
- data/lib/dash/autoscale/lease_held.rb +2 -0
- data/lib/dash/autoscale/lease_lost.rb +2 -0
- data/lib/dash/autoscale/member.rb +28 -0
- data/lib/dash/autoscale/metrics.rb +70 -0
- data/lib/dash/autoscale/metrics_server.rb +71 -0
- data/lib/dash/autoscale/observation.rb +41 -0
- data/lib/dash/autoscale/pause.rb +34 -0
- data/lib/dash/autoscale/policy.rb +198 -0
- data/lib/dash/autoscale/pool.rb +103 -0
- data/lib/dash/autoscale/prometheus_client.rb +76 -0
- data/lib/dash/autoscale/provider/base.rb +43 -0
- data/lib/dash/autoscale/provider/exec.rb +94 -0
- data/lib/dash/autoscale/provider/upcloud.rb +140 -0
- data/lib/dash/autoscale/provider.rb +9 -0
- data/lib/dash/autoscale/provider_error.rb +4 -0
- data/lib/dash/autoscale/read_only_query.rb +38 -0
- data/lib/dash/autoscale/reader.rb +17 -0
- data/lib/dash/autoscale/recorder.rb +13 -0
- data/lib/dash/autoscale/role_state.rb +110 -0
- data/lib/dash/autoscale/sample.rb +13 -0
- data/lib/dash/autoscale/signal/pgbus.rb +19 -0
- data/lib/dash/autoscale/signal/prometheus.rb +8 -0
- data/lib/dash/autoscale/signal/sidekiq.rb +49 -0
- data/lib/dash/autoscale/signal.rb +31 -0
- data/lib/dash/autoscale/simulation.rb +200 -0
- data/lib/dash/autoscale/state_store.rb +132 -0
- data/lib/dash/autoscale/timestamp.rb +15 -0
- data/lib/dash/autoscale/tunnel.rb +48 -0
- data/lib/dash/cli/accessory.rb +16 -0
- data/lib/dash/cli/app/boot.rb +69 -44
- data/lib/dash/cli/app/boot_state.rb +70 -0
- data/lib/dash/cli/app/rollout_boot.rb +28 -9
- data/lib/dash/cli/app/running_targets.rb +39 -0
- data/lib/dash/cli/app/slot_version.rb +29 -0
- data/lib/dash/cli/app.rb +89 -32
- data/lib/dash/cli/autoscale.rb +312 -0
- data/lib/dash/cli/base.rb +15 -3
- data/lib/dash/cli/lock.rb +6 -1
- data/lib/dash/cli/main.rb +77 -8
- data/lib/dash/cli/proxy/inspection.rb +52 -0
- data/lib/dash/cli/proxy/loadbalancer_deploy.rb +20 -0
- data/lib/dash/cli/proxy/loadbalancer_reboot.rb +1 -1
- data/lib/dash/cli/proxy/reboot.rb +4 -9
- data/lib/dash/cli/proxy.rb +12 -7
- data/lib/dash/cli/prune.rb +5 -3
- data/lib/dash/cli/report.rb +4 -1
- data/lib/dash/cli/scale/host_plan.rb +63 -0
- data/lib/dash/cli/scale/join.rb +128 -0
- data/lib/dash/cli/scale/leave.rb +58 -0
- data/lib/dash/cli/scale/placement.rb +54 -0
- data/lib/dash/cli/scale/power_off.rb +26 -0
- data/lib/dash/cli/scale/replica_join.rb +74 -0
- data/lib/dash/cli/scale/replica_leave.rb +121 -0
- data/lib/dash/cli/scale.rb +276 -0
- data/lib/dash/cli/server.rb +41 -0
- data/lib/dash/cli/stats_table.rb +34 -0
- data/lib/dash/cli/templates/sample_hooks/post-scale-in.sample +3 -0
- data/lib/dash/cli/templates/sample_hooks/post-scale-out.sample +3 -0
- data/lib/dash/cli/templates/sample_hooks/pre-scale-in.sample +6 -0
- data/lib/dash/cli/templates/sample_hooks/pre-scale-out.sample +6 -0
- data/lib/dash/commander.rb +17 -3
- data/lib/dash/commands/accessory/proxy.rb +1 -1
- data/lib/dash/commands/accessory.rb +21 -2
- data/lib/dash/commands/app/containers.rb +28 -3
- data/lib/dash/commands/app/logging.rb +13 -2
- data/lib/dash/commands/app/proxy.rb +4 -4
- data/lib/dash/commands/app.rb +90 -10
- data/lib/dash/commands/auditor.rb +2 -2
- data/lib/dash/commands/autoscale.rb +133 -0
- data/lib/dash/commands/base.rb +4 -0
- data/lib/dash/commands/proxy/services.rb +10 -0
- data/lib/dash/commands/prune.rb +10 -2
- data/lib/dash/commands/server.rb +16 -0
- data/lib/dash/configuration/accessory.rb +2 -2
- data/lib/dash/configuration/autoscale.rb +123 -0
- data/lib/dash/configuration/docs/autoscale.yml +141 -0
- data/lib/dash/configuration/docs/configuration.yml +8 -2
- data/lib/dash/configuration/docs/proxy.yml +1 -1
- data/lib/dash/configuration/docs/role.yml +203 -0
- data/lib/dash/configuration/loadbalancer.rb +3 -2
- data/lib/dash/configuration/proxy/run.rb +1 -1
- data/lib/dash/configuration/proxy.rb +19 -7
- data/lib/dash/configuration/role/drain.rb +49 -0
- data/lib/dash/configuration/role/replicas.rb +72 -0
- data/lib/dash/configuration/role/scale/guard.rb +40 -0
- data/lib/dash/configuration/role/scale/signal.rb +93 -0
- data/lib/dash/configuration/role/scale/window.rb +37 -0
- data/lib/dash/configuration/role/scale.rb +199 -0
- data/lib/dash/configuration/role.rb +123 -6
- data/lib/dash/configuration/validator/autoscale.rb +126 -0
- data/lib/dash/configuration/validator/proxy.rb +13 -0
- data/lib/dash/configuration/validator/role.rb +26 -5
- data/lib/dash/configuration.rb +126 -15
- data/lib/dash/diagnostics/audit.rb +28 -0
- data/lib/dash/diagnostics/autoscale_decisions.rb +18 -0
- data/lib/dash/diagnostics/autoscale_explain.rb +30 -0
- data/lib/dash/diagnostics/autoscale_signal.rb +82 -0
- data/lib/dash/diagnostics/base.rb +53 -0
- data/lib/dash/diagnostics/config.rb +32 -0
- data/lib/dash/diagnostics/container_stats.rb +67 -0
- data/lib/dash/diagnostics/containers.rb +20 -0
- data/lib/dash/diagnostics/controller_status.rb +44 -0
- data/lib/dash/diagnostics/docker_ps.rb +40 -0
- data/lib/dash/{cli → diagnostics}/doctor/config_checks.rb +3 -3
- data/lib/dash/diagnostics/doctor/controller_checks.rb +46 -0
- data/lib/dash/{cli → diagnostics}/doctor/endpoint_checks.rb +12 -4
- data/lib/dash/{cli → diagnostics}/doctor/host_checks.rb +5 -4
- data/lib/dash/diagnostics/doctor/pool_checks.rb +65 -0
- data/lib/dash/diagnostics/doctor/signal_checks.rb +54 -0
- data/lib/dash/diagnostics/doctor.rb +182 -0
- data/lib/dash/diagnostics/drift.rb +168 -0
- data/lib/dash/diagnostics/host_stats.rb +59 -0
- data/lib/dash/diagnostics/lines.rb +14 -0
- data/lib/dash/diagnostics/lock.rb +27 -0
- data/lib/dash/diagnostics/logs.rb +76 -0
- data/lib/dash/diagnostics/pool.rb +41 -0
- data/lib/dash/diagnostics/proxy_services.rb +36 -0
- data/lib/dash/diagnostics/redactor.rb +48 -0
- data/lib/dash/diagnostics/reports.rb +17 -0
- data/lib/dash/diagnostics/scale.rb +51 -0
- data/lib/dash/diagnostics/units.rb +31 -0
- data/lib/dash/mcp/base_tool.rb +43 -0
- data/lib/dash/mcp/runner.rb +28 -0
- data/lib/dash/mcp/server.rb +40 -0
- data/lib/dash/mcp/session.rb +51 -0
- data/lib/dash/mcp/tools/audit_tool.rb +11 -0
- data/lib/dash/mcp/tools/autoscale_decisions_tool.rb +22 -0
- data/lib/dash/mcp/tools/autoscale_explain_tool.rb +56 -0
- data/lib/dash/mcp/tools/config_tool.rb +10 -0
- data/lib/dash/mcp/tools/container_stats_tool.rb +15 -0
- data/lib/dash/mcp/tools/containers_tool.rb +10 -0
- data/lib/dash/mcp/tools/controller_status_tool.rb +18 -0
- data/lib/dash/mcp/tools/deploy_reports_tool.rb +10 -0
- data/lib/dash/mcp/tools/doctor_tool.rb +13 -0
- data/lib/dash/mcp/tools/drift_tool.rb +16 -0
- data/lib/dash/mcp/tools/host_stats_tool.rb +14 -0
- data/lib/dash/mcp/tools/lock_status_tool.rb +15 -0
- data/lib/dash/mcp/tools/logs_tool.rb +41 -0
- data/lib/dash/mcp/tools/pool_members_tool.rb +15 -0
- data/lib/dash/mcp/tools/proxy_services_tool.rb +10 -0
- data/lib/dash/mcp/tools/scale_status_tool.rb +10 -0
- data/lib/dash/mcp/tools/signal_sample_tool.rb +19 -0
- data/lib/dash/mcp/transport.rb +14 -0
- data/lib/dash/mcp.rb +56 -0
- data/lib/dash/sshkit_with_ext.rb +4 -4
- data/lib/dash/utils.rb +7 -0
- data/lib/dash/version.rb +1 -1
- data/lib/dash.rb +2 -0
- metadata +119 -5
- data/lib/dash/cli/doctor.rb +0 -113
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
# One line of the app's `docker ps` in Commands::App::Containers::PS_WITH_ROLE_FORMAT - the
|
|
4
|
+
# container's JSON, a tab, docker's own value of its `role` label - as a container of this
|
|
5
|
+
# deploy. The role comes from that label value, never from the name alone (a sibling role
|
|
6
|
+
# whose name extends another's would be misattributed) and never from the rendered `Labels`
|
|
7
|
+
# string (a comma in another label's value could pass for a role there).
|
|
8
|
+
module Dash::Diagnostics::DockerPs
|
|
9
|
+
HEALTH = /\((healthy|unhealthy|health: starting)\)/
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# An accessory's containers carry only its `service` label, so they are named for the
|
|
14
|
+
# accessory the caller asked about rather than for a role; their lines are plain JSON.
|
|
15
|
+
def containers(output, accessory: nil)
|
|
16
|
+
output.to_s.lines.filter_map do |line|
|
|
17
|
+
next if line.strip.empty?
|
|
18
|
+
|
|
19
|
+
ps, role = line.chomp.split("\t", 2)
|
|
20
|
+
accessory ? accessory_container(JSON.parse(ps), accessory) : container(JSON.parse(ps), role && JSON.parse(role))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def accessory_container(ps, accessory)
|
|
25
|
+
{ name: ps["Names"], id: ps["ID"], accessory: accessory, state: ps["State"], status: ps["Status"], health: ps["Status"].to_s[HEALTH, 1],
|
|
26
|
+
image: ps["Image"], created_at: ps["CreatedAt"] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def container(ps, role_name)
|
|
30
|
+
role = DASH.config.role(role_name) if role_name.present?
|
|
31
|
+
name = ps["Names"]
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
name: name, id: ps["ID"], role: role_name.presence,
|
|
35
|
+
replica: role&.replica_from_name(name), version: role&.version_from_name(name),
|
|
36
|
+
state: ps["State"], status: ps["Status"], health: ps["Status"].to_s[HEALTH, 1],
|
|
37
|
+
image: ps["Image"], created_at: ps["CreatedAt"]
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Checks that only read the configuration — no SSH, no network. They report problems
|
|
2
2
|
# that are visible in deploy.yml alone, so they still run when every host is unreachable.
|
|
3
|
-
class Dash::
|
|
3
|
+
class Dash::Diagnostics::Doctor::ConfigChecks
|
|
4
4
|
def run
|
|
5
5
|
readiness_results + dockerfile_results
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
private
|
|
9
9
|
def result(check, target, status, detail)
|
|
10
|
-
Dash::
|
|
10
|
+
Dash::Diagnostics::Doctor::Result.new(check, target, status, detail)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def readiness_results
|
|
14
|
-
|
|
14
|
+
Dash::Diagnostics::Doctor.scoped_roles.map { |role| readiness_check(role) }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def readiness_check(role)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Whether the autoscale controller is running, for apps that need one: a role with a
|
|
2
|
+
# `scale.schedule` or `scale.signal` changes capacity only while `dash autoscale run` ticks. A missing,
|
|
3
|
+
# stale or stopped heartbeat warns, and so does every pause still in force. A state host
|
|
4
|
+
# that does not answer is a warning, never an exception.
|
|
5
|
+
class Dash::Diagnostics::Doctor::ControllerChecks
|
|
6
|
+
def initialize(now: Time.now.utc)
|
|
7
|
+
@now = now
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Within the run's --roles, like the other checks.
|
|
11
|
+
def run
|
|
12
|
+
roles = Dash::Diagnostics::Doctor.scoped_roles
|
|
13
|
+
return [] if roles.none?(&:controlled?)
|
|
14
|
+
|
|
15
|
+
status = Dash::Diagnostics::ControllerStatus.new(now: @now, roles: roles).to_h
|
|
16
|
+
return [ result(status[:state_host], :warn, "could not read the controller heartbeat (#{status[:error]})") ] if status[:error]
|
|
17
|
+
|
|
18
|
+
[ controller_result(status), *status[:pauses].map { |pause| pause_result(pause) } ]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
def result(target, status, detail)
|
|
23
|
+
Dash::Diagnostics::Doctor::Result.new(:controller, target, status, detail)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def controller_result(status)
|
|
27
|
+
host, controller = status[:state_host], status[:controller]
|
|
28
|
+
return result(host, :warn, "no controller has run; #{status[:roles].join(", ")} have a schedule or signal that only dash autoscale run carries out") unless controller
|
|
29
|
+
|
|
30
|
+
name = "controller #{controller[:controller_id]} on #{controller[:hostname]}"
|
|
31
|
+
|
|
32
|
+
case controller[:status]
|
|
33
|
+
when "running"
|
|
34
|
+
result host, :ok, "#{name} (dash #{controller[:version]}) ticked #{controller[:age_seconds]}s ago, for #{status[:roles].join(", ")}"
|
|
35
|
+
when "stopped"
|
|
36
|
+
result host, :warn, "#{name} stopped at #{controller[:stopped_at]}"
|
|
37
|
+
else
|
|
38
|
+
result host, :warn, "#{name} last ticked #{controller[:age_seconds]}s ago (interval #{controller[:interval]}s), it looks stopped or stuck"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def pause_result(pause)
|
|
43
|
+
until_text = pause[:until] == Dash::Autoscale::Pause::INDEFINITE ? "until resumed" : "until #{pause[:until]}"
|
|
44
|
+
result pause[:role], :warn, "paused by #{pause[:by] || "unknown"} #{until_text}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -4,7 +4,7 @@ require "openssl"
|
|
|
4
4
|
|
|
5
5
|
# Local (no SSH) readiness checks for the domains dash-proxy will serve:
|
|
6
6
|
# DNS resolution against the configured hosts and TLS certificate expiry.
|
|
7
|
-
class Dash::
|
|
7
|
+
class Dash::Diagnostics::Doctor::EndpointChecks
|
|
8
8
|
CERTIFICATE_EXPIRY_WARN_DAYS = 14
|
|
9
9
|
SECONDS_PER_DAY = 86_400
|
|
10
10
|
TLS_CONNECT_TIMEOUT = 5
|
|
@@ -15,22 +15,30 @@ class Dash::Cli::Doctor::EndpointChecks
|
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
def result(check, target, status, detail)
|
|
18
|
-
Dash::
|
|
18
|
+
Dash::Diagnostics::Doctor::Result.new(check, target, status, detail)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Roles and accessories that run behind the proxy with custom domains.
|
|
22
22
|
def proxied_units
|
|
23
|
-
@proxied_units ||= (
|
|
23
|
+
@proxied_units ||= (Dash::Diagnostics::Doctor.scoped_roles + DASH.config.proxy_accessories).select do |unit|
|
|
24
24
|
unit.running_proxy? && unit.proxy.hosts.any?
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def dns_results
|
|
29
29
|
proxied_units.flat_map do |unit|
|
|
30
|
-
unit.proxy.hosts.map { |domain|
|
|
30
|
+
unit.proxy.hosts.map { |domain| dns_check_of(unit, domain) }
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# A scaled role's hosts come from the autoscale pool; when it cannot be read, its DNS
|
|
35
|
+
# cannot be compared, but its certificates still can.
|
|
36
|
+
def dns_check_of(unit, domain)
|
|
37
|
+
dns_check(domain, unit.hosts, acme_issued: acme_issued?(unit))
|
|
38
|
+
rescue Dash::Autoscale::ProviderError => e
|
|
39
|
+
result :dns, domain, :fail, "not compared, the hosts of #{unit.name} are unknown (#{e.message})"
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
# Whether this unit's certificate comes from ACME rather than the operator.
|
|
35
43
|
# Only then does where the domain points decide whether a certificate can
|
|
36
44
|
# exist — a supplied certificate is unaffected by DNS.
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
# Every check rescues StandardError at its boundary: the doctor's contract is
|
|
4
4
|
# to report broken environments, not crash on them, and a failed connection can
|
|
5
5
|
# raise anything from Net::SSH errors to Errno and DNS resolution errors.
|
|
6
|
-
class Dash::
|
|
6
|
+
class Dash::Diagnostics::Doctor::HostChecks
|
|
7
7
|
attr_reader :host, :sshkit, :proxy_host
|
|
8
8
|
delegate :execute, :capture_with_info, to: :sshkit
|
|
9
9
|
|
|
10
|
-
def initialize(host, sshkit, proxy_host:)
|
|
10
|
+
def initialize(host, sshkit, proxy_host:, registry: true)
|
|
11
11
|
@host = host
|
|
12
12
|
@sshkit = sshkit
|
|
13
13
|
@proxy_host = proxy_host
|
|
14
|
+
@registry = registry
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def run
|
|
@@ -18,7 +19,7 @@ class Dash::Cli::Doctor::HostChecks
|
|
|
18
19
|
|
|
19
20
|
if checks[:ssh].ok?
|
|
20
21
|
checks[:docker] = docker_check
|
|
21
|
-
checks[:registry] = registry_check
|
|
22
|
+
checks[:registry] = registry_check if @registry
|
|
22
23
|
checks.merge!(proxy_checks) if proxy_host
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -27,7 +28,7 @@ class Dash::Cli::Doctor::HostChecks
|
|
|
27
28
|
|
|
28
29
|
private
|
|
29
30
|
def result(check, status, detail)
|
|
30
|
-
Dash::
|
|
31
|
+
Dash::Diagnostics::Doctor::Result.new(check, host, status, detail)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def ssh_check
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Checks on the pool members of scaled roles: whether the provider answers, whether a
|
|
2
|
+
# `power` role has enough members to reach its max, which members are stuck in a state
|
|
3
|
+
# that is neither on nor off, and whether a scaled role behind dash-proxy is load balanced.
|
|
4
|
+
# A provider failure is a failing result, never an exception.
|
|
5
|
+
class Dash::Diagnostics::Doctor::PoolChecks
|
|
6
|
+
def run
|
|
7
|
+
DASH.config.scaled_roles.flat_map { |role| role_results(role) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
def result(target, status, detail)
|
|
12
|
+
Dash::Diagnostics::Doctor::Result.new(:pool, target, status, detail)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def role_results(role)
|
|
16
|
+
members = role.members
|
|
17
|
+
return [ unverified_result(role, members), loadbalancer_result(role) ].compact if members.any? { |member| !member.verified }
|
|
18
|
+
|
|
19
|
+
[ provider_result(role, members), capacity_result(role, members), *stuck_results(role, members), loadbalancer_result(role) ].compact
|
|
20
|
+
rescue Dash::Autoscale::ProviderError => e
|
|
21
|
+
[ result(role.name, :fail, e.message), loadbalancer_result(role) ].compact
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def provider_result(role, members)
|
|
25
|
+
active = members.count(&:started?)
|
|
26
|
+
result role.name, :ok, "#{DASH.config.autoscale.provider_name} answered: #{members.size} #{"member".pluralize(members.size)}, #{active} started"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Only --hosts produces unverified members, and only when the provider did not answer.
|
|
30
|
+
def unverified_result(role, members)
|
|
31
|
+
result role.name, :warn, "#{DASH.config.autoscale.provider_name} did not answer: #{members.size} #{"member".pluralize(members.size)} " \
|
|
32
|
+
"taken from --hosts, unverified (#{members.map(&:host).join(", ")})"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def capacity_result(role, members)
|
|
36
|
+
if role.scale.create?
|
|
37
|
+
result role.name, :ok, "creates members from its template, up to #{role.scale.max} hosts"
|
|
38
|
+
else
|
|
39
|
+
needed = role.scale.max - role.baseline_hosts.size
|
|
40
|
+
|
|
41
|
+
if members.size >= needed
|
|
42
|
+
result role.name, :ok, "#{members.size} #{"member".pluralize(members.size)} for the #{needed} it can scale out to (max #{role.scale.max} hosts)"
|
|
43
|
+
else
|
|
44
|
+
result role.name, :warn, "#{members.size} of the #{needed} members max #{role.scale.max} needs carry its labels " \
|
|
45
|
+
"(#{DASH.config.pool.labels_for(role).map { |key, value| "#{key}=#{value}" }.join(", ")}); it cannot scale past #{role.baseline_hosts.size + members.size} hosts"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stuck_results(role, members)
|
|
51
|
+
members.select(&:transitional?).map do |member|
|
|
52
|
+
result "#{role.name} #{member.id}", :warn, "member #{member.host || member.id} is #{member.state}, dash leaves it alone until it is started or stopped"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def loadbalancer_result(role)
|
|
57
|
+
return unless role.running_proxy?
|
|
58
|
+
|
|
59
|
+
if DASH.config.proxy.load_balancing?
|
|
60
|
+
result role.name, :ok, "load balanced by #{DASH.config.proxy.effective_loadbalancer}"
|
|
61
|
+
else
|
|
62
|
+
result role.name, :fail, "a scaled role behind dash-proxy needs the load balancer"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Whether each signalled role's `scale.signal` source and `scale.hold_when` guards answer,
|
|
2
|
+
# through Dash::Diagnostics::AutoscaleSignal - the reads the controller makes. Each result
|
|
3
|
+
# names the value, the latency and the machine it was read from: a doctor run on a laptop
|
|
4
|
+
# reaches the sources by another path than the controller host does. A source that does
|
|
5
|
+
# not answer fails; one that answers without a number, and a guard holding a scale-out, warn.
|
|
6
|
+
class Dash::Diagnostics::Doctor::SignalChecks
|
|
7
|
+
# Within the run's --roles, like the other checks. No signalled role: no result.
|
|
8
|
+
# One set of connections (and `via:` tunnels) for every role: the sources are the app's.
|
|
9
|
+
def run
|
|
10
|
+
roles = Dash::Diagnostics::Doctor.scoped_roles.select { |role| role.scaled? && role.scale.signal? }
|
|
11
|
+
return [] if roles.empty?
|
|
12
|
+
|
|
13
|
+
connections = Dash::Autoscale::Connections.new
|
|
14
|
+
begin
|
|
15
|
+
roles.flat_map { |role| role_results(role, connections) }
|
|
16
|
+
ensure
|
|
17
|
+
connections.close
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
def role_results(role, connections)
|
|
23
|
+
sample = Dash::Diagnostics::AutoscaleSignal.new(role: role, connections: connections).to_h
|
|
24
|
+
from = "from this machine (#{sample[:from]})"
|
|
25
|
+
|
|
26
|
+
[ source_result(role.name, sample[:signal], from),
|
|
27
|
+
*sample[:guards].map { |guard| guard_result("#{role.name} hold_when/#{guard[:index]}", guard, from) } ]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def source_result(target, read, from)
|
|
31
|
+
if read[:error]
|
|
32
|
+
result target, :fail, "#{read[:source]} did not answer #{from}: #{read[:error]}"
|
|
33
|
+
elsif read[:value].nil?
|
|
34
|
+
result target, :warn, "#{read[:source]} answered with no value in #{read[:latency_ms]}ms, #{from}"
|
|
35
|
+
else
|
|
36
|
+
result target, :ok, "#{read[:source]} answered #{read[:value]} in #{read[:latency_ms]}ms, #{from}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def guard_result(target, guard, from)
|
|
41
|
+
return source_result(target, guard, from) if guard[:error]
|
|
42
|
+
return result(target, :warn, "#{guard[:source]} answered with no value in #{guard[:latency_ms]}ms, #{from}; without a value it holds a scale-out") if guard[:value].nil?
|
|
43
|
+
|
|
44
|
+
if guard[:holding]
|
|
45
|
+
result target, :warn, "#{guard[:source]} answered #{guard[:value]} in #{guard[:latency_ms]}ms, #{from}; above #{guard[:above]}, so it holds a scale-out"
|
|
46
|
+
else
|
|
47
|
+
source_result(target, guard, from)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def result(target, status, detail)
|
|
52
|
+
Dash::Diagnostics::Doctor::Result.new(:signal, target, status, detail)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# `dash doctor`: deploy readiness checks, collected as results without ever raising on a
|
|
2
|
+
# broken environment - failures become failing results instead. `to_h` is the same as data.
|
|
3
|
+
#
|
|
4
|
+
# Every check reads, except the registry check: it runs `docker login` on every host,
|
|
5
|
+
# which writes the registry credentials there. `registry: false` leaves it out, and
|
|
6
|
+
# `dash mcp` asks for that.
|
|
7
|
+
class Dash::Diagnostics::Doctor < Dash::Diagnostics::Base
|
|
8
|
+
REGISTRY_SKIPPED = { check: "registry", reason: "runs docker login on each host, which writes credentials there; run `dash doctor` to include it" }.freeze
|
|
9
|
+
|
|
10
|
+
HOST_CHECKS = %i[ ssh docker registry proxy_image proxy_version proxy_socket ports ]
|
|
11
|
+
|
|
12
|
+
CHECK_TITLES = {
|
|
13
|
+
ssh: "SSH",
|
|
14
|
+
docker: "Docker",
|
|
15
|
+
registry: "Registry",
|
|
16
|
+
proxy_image: "Proxy image",
|
|
17
|
+
proxy_version: "Proxy version",
|
|
18
|
+
proxy_socket: "Proxy docker socket",
|
|
19
|
+
ports: "Ports",
|
|
20
|
+
dns: "DNS",
|
|
21
|
+
certificate: "Certificates",
|
|
22
|
+
readiness: "Readiness",
|
|
23
|
+
dockerfile: "Dockerfile",
|
|
24
|
+
drift: "Drift",
|
|
25
|
+
pool: "Pool",
|
|
26
|
+
controller: "Controller",
|
|
27
|
+
signal: "Signals"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
STATUS_COLORS = { ok: :green, warn: :yellow, fail: :red }.freeze
|
|
31
|
+
|
|
32
|
+
Result = Struct.new(:check, :target, :status, :detail) do
|
|
33
|
+
def ok?
|
|
34
|
+
status == :ok
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def warn?
|
|
38
|
+
status == :warn
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fail?
|
|
42
|
+
status == :fail
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def title
|
|
46
|
+
CHECK_TITLES[check]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
"#{status.to_s.upcase} #{target}: #{detail}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :results
|
|
55
|
+
|
|
56
|
+
# The roles this run is about. DASH.roles builds the host scope, which asks the autoscale
|
|
57
|
+
# pool - so it is only used when --hosts narrowed the run, and the pool has been read
|
|
58
|
+
# already to filter by them. Otherwise --roles, or every role, straight from deploy.yml.
|
|
59
|
+
def self.scoped_roles
|
|
60
|
+
DASH.specific_hosts ? DASH.roles : (DASH.specific_roles || DASH.config.roles)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(registry: true)
|
|
64
|
+
@results = []
|
|
65
|
+
@registry = registry
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Pool checks first: when the provider cannot be asked, the member hosts are unknown, so
|
|
69
|
+
# the checks that need every host report that once instead of crashing on it.
|
|
70
|
+
def run
|
|
71
|
+
pool = pool_check_results
|
|
72
|
+
@results = without_pool(:ssh) { host_check_results } + without_pool(:dns) { endpoint_check_results } +
|
|
73
|
+
config_check_results + pool + controller_check_results + signal_check_results + drift_check_results
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def failures
|
|
77
|
+
results.select(&:fail?)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def warnings
|
|
81
|
+
results.select(&:warn?)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def successful?
|
|
85
|
+
failures.none?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
def snapshot
|
|
90
|
+
run
|
|
91
|
+
|
|
92
|
+
{ successful: successful?, results: results.map { |result| result_h(result) }, skipped: (@registry ? [] : [ REGISTRY_SKIPPED ]) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def result_h(result)
|
|
96
|
+
{ check: result.check.to_s, target: result.target.to_s, status: result.status.to_s, detail: result.detail.to_s }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def host_check_results
|
|
100
|
+
results_by_host = collect_host_checks
|
|
101
|
+
|
|
102
|
+
HOST_CHECKS.flat_map do |check|
|
|
103
|
+
DASH.hosts.filter_map { |host| results_by_host.dig(host, check) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def collect_host_checks
|
|
108
|
+
results_by_host = {}
|
|
109
|
+
mutex = Mutex.new
|
|
110
|
+
proxy_hosts = DASH.proxy_hosts
|
|
111
|
+
registry = @registry
|
|
112
|
+
error = nil
|
|
113
|
+
|
|
114
|
+
begin
|
|
115
|
+
on(DASH.hosts) do |host|
|
|
116
|
+
checks = Dash::Diagnostics::Doctor::HostChecks.new(host.hostname, self, proxy_host: proxy_hosts.include?(host.hostname), registry: registry).run
|
|
117
|
+
mutex.synchronize { results_by_host[host.hostname] = checks }
|
|
118
|
+
end
|
|
119
|
+
# Only ExecuteError: sshkit 1.25 has no MultipleExecuteError, and naming
|
|
120
|
+
# a constant that does not exist turns "a host's checks failed" into a
|
|
121
|
+
# NameError - the opposite of the doctor's never-crash contract.
|
|
122
|
+
rescue SSHKit::Runner::ExecuteError => e
|
|
123
|
+
# Per-check errors are captured inside HostChecks; getting here means a
|
|
124
|
+
# host's checks never completed at all. Record those as SSH failures below.
|
|
125
|
+
error = e
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
DASH.hosts.each do |host|
|
|
129
|
+
results_by_host[host] ||= { ssh: Result.new(:ssh, host, :fail, "could not run checks (#{error&.message || "connection failed"})") }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
results_by_host
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def endpoint_check_results
|
|
136
|
+
Dash::Diagnostics::Doctor::EndpointChecks.new.run
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def config_check_results
|
|
140
|
+
Dash::Diagnostics::Doctor::ConfigChecks.new.run
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def pool_check_results
|
|
144
|
+
Dash::Diagnostics::Doctor::PoolChecks.new.run
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def controller_check_results
|
|
148
|
+
Dash::Diagnostics::Doctor::ControllerChecks.new.run
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
[ Result.new(:controller, "controller", :warn, "could not check the autoscale controller (#{e.class}: #{e.message})") ]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def signal_check_results
|
|
154
|
+
Dash::Diagnostics::Doctor::SignalChecks.new.run
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
[ Result.new(:signal, "signals", :warn, "could not check the autoscale signals (#{e.class}: #{e.message})") ]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def without_pool(check)
|
|
160
|
+
yield
|
|
161
|
+
rescue Dash::Autoscale::ProviderError => e
|
|
162
|
+
[ Result.new(check, "pool", :fail, "not run, the pool members are unknown (#{e.message})") ]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Whether the proxies route to what runs. A target that is not running, or a host the
|
|
166
|
+
# load balancer should forward to and does not, fails; the rest warn.
|
|
167
|
+
def drift_check_results
|
|
168
|
+
drift = Dash::Diagnostics::Drift.take
|
|
169
|
+
unread = drift.unread.map { |host| Result.new(:drift, host[:host], :warn, "could not read #{host[:source]} (#{host[:error]}), so it was not compared") }
|
|
170
|
+
|
|
171
|
+
if drift.entries.empty? && unread.empty?
|
|
172
|
+
[ Result.new(:drift, "proxy", :ok, "proxy targets match the running containers") ]
|
|
173
|
+
else
|
|
174
|
+
unread + drift.entries.map do |entry|
|
|
175
|
+
status = Dash::Diagnostics::Drift::FAILURES.include?(entry[:code]) ? :fail : :warn
|
|
176
|
+
Result.new(:drift, entry[:host] || entry[:role], status, "#{entry[:code]}: #{entry[:detail]}")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
[ Result.new(:drift, "proxy", :warn, "could not compare proxy targets with containers (#{e.class}: #{e.message})") ]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Compares what should be running with what is: the containers on each host against the
|
|
2
|
+
# targets dash-proxy routes to there, against the load balancer's targets. Pure - it reads
|
|
3
|
+
# the Containers, ProxyServices and Lock snapshots and touches nothing; `.take` captures
|
|
4
|
+
# them first.
|
|
5
|
+
#
|
|
6
|
+
# Each mismatch is `{ code:, host:, role:, detail: }`, with a code an agent can branch on:
|
|
7
|
+
#
|
|
8
|
+
# proxy_target_not_running the proxy routes to a container that is not running
|
|
9
|
+
# running_not_targeted a running container of a proxied role is not a target
|
|
10
|
+
# loadbalancer_target_missing a host the load balancer should forward to is not a target
|
|
11
|
+
# loadbalancer_target_extra the load balancer forwards to a host it should not
|
|
12
|
+
# version_mismatch a role's hosts run different versions
|
|
13
|
+
# multiple_running_versions one host runs more than one version of a role
|
|
14
|
+
# member_not_targeted a started pool member the load balancer does not forward to
|
|
15
|
+
# (reported instead of loadbalancer_target_missing for it)
|
|
16
|
+
# member_orphan a started pool member runs nothing of its role
|
|
17
|
+
#
|
|
18
|
+
# A host whose snapshot is an error is not compared; it is listed under `unread` instead,
|
|
19
|
+
# and the fleet is not called consistent.
|
|
20
|
+
# While the deploy lock is held the version codes are skipped too - a deploy in flight runs
|
|
21
|
+
# two versions on purpose - and `lock_held` says so (nil when the lock was not read).
|
|
22
|
+
class Dash::Diagnostics::Drift < Dash::Diagnostics::Base
|
|
23
|
+
FAILURES = %w[ proxy_target_not_running loadbalancer_target_missing ].freeze
|
|
24
|
+
REPLACED_SUFFIX = /_replaced_\h{16}\z/
|
|
25
|
+
TARGET_KEYS = %w[ targets reader_targets rollout_targets ].freeze
|
|
26
|
+
|
|
27
|
+
# The lock is only read when the primary host is in scope; narrowed away from it, the
|
|
28
|
+
# version codes are reported whether or not a deploy is in flight.
|
|
29
|
+
def self.take
|
|
30
|
+
lock = Dash::Diagnostics::Lock.new.to_h if DASH.hosts.include?(DASH.config.primary_host)
|
|
31
|
+
|
|
32
|
+
new(containers: Dash::Diagnostics::Containers.new(accessories: []).to_h, proxy_services: Dash::Diagnostics::ProxyServices.new.to_h, lock: lock)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(containers:, proxy_services:, lock: nil, config: nil)
|
|
36
|
+
@containers = containers
|
|
37
|
+
@proxy_services = proxy_services
|
|
38
|
+
@lock = lock
|
|
39
|
+
@config = config
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def entries
|
|
43
|
+
@entries ||= proxy_entries + loadbalancer_entries + member_entries + version_entries
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The hosts whose containers or routes could not be read, so were not compared. A fleet
|
|
47
|
+
# with any is not reported consistent: no drift found is not the same as none there.
|
|
48
|
+
def unread
|
|
49
|
+
@unread ||= [ [ "containers", Array(@containers[:hosts]) ], [ "proxy_services", [ *@proxy_services[:hosts], @proxy_services[:loadbalancer] ].compact ] ]
|
|
50
|
+
.flat_map { |source, hosts| hosts.select { |host| host[:error] }.map { |host| { host: host[:host], source: source, error: host[:error] } } }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
def snapshot
|
|
55
|
+
{ consistent: entries.empty? && unread.empty?, lock_held: @lock&.dig(:lock, :held), drift: entries, unread: unread }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def proxy_entries
|
|
59
|
+
Array(@proxy_services[:hosts]).reject { |host| host[:error] }.flat_map do |proxy_host|
|
|
60
|
+
host = proxy_host[:host]
|
|
61
|
+
next [] unless running.key?(host)
|
|
62
|
+
|
|
63
|
+
proxied_roles_on(host).flat_map { |role| role_proxy_entries(role, host, targets(proxy_host[:services].to_h[role.container_prefix])) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def role_proxy_entries(role, host, targets)
|
|
68
|
+
containers = running[host].select { |container| container[:role] == role.name }
|
|
69
|
+
|
|
70
|
+
not_running = targets.reject { |target| containers.any? { |container| container[:id].to_s.start_with?(target) } }.map do |target|
|
|
71
|
+
entry "proxy_target_not_running", host, role, "#{role.container_prefix} routes to #{target}, which is not a running container"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
not_targeted = containers.reject { |container| targets.any? { |target| container[:id].to_s.start_with?(target) } }.map do |container|
|
|
75
|
+
entry "running_not_targeted", host, role, "#{container[:name]} (replica #{container[:replica]}) runs but #{role.container_prefix} does not route to it"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
not_running + not_targeted
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def loadbalancer_entries
|
|
82
|
+
loadbalancer = @proxy_services[:loadbalancer]
|
|
83
|
+
return [] if loadbalancer.nil? || loadbalancer[:error]
|
|
84
|
+
|
|
85
|
+
expected = Dash::Configuration::Loadbalancer.new(config: config, proxy_config: config.proxy.proxy_config, secrets: config.secrets).target_hosts.uniq
|
|
86
|
+
actual = targets(loadbalancer[:services].to_h[config.service])
|
|
87
|
+
host = loadbalancer[:host]
|
|
88
|
+
|
|
89
|
+
(expected - actual).map { |target| missing_target_entry(host, target) } +
|
|
90
|
+
(actual - expected).map { |target| entry "loadbalancer_target_extra", host, nil, "the load balancer forwards to #{target}, which no proxied role runs on" }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A member that joined but never reached the load balancer serves nobody: capacity paid
|
|
94
|
+
# for and wasted, not a broken route - so it is its own code, not a failure.
|
|
95
|
+
def missing_target_entry(host, target)
|
|
96
|
+
if (member = started_members.find { |candidate| candidate.host == target })
|
|
97
|
+
role = config.roles.find { |candidate| candidate.name == member.role }
|
|
98
|
+
entry "member_not_targeted", host, role, "member #{member.id} (#{target}) is started but the load balancer does not forward to it"
|
|
99
|
+
else
|
|
100
|
+
entry "loadbalancer_target_missing", host, nil, "the load balancer does not forward to #{target}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def member_entries
|
|
105
|
+
config.scaled_roles.flat_map do |role|
|
|
106
|
+
role.active_members.filter_map do |member|
|
|
107
|
+
next unless running.key?(member.host)
|
|
108
|
+
next if running[member.host].any? { |container| container[:role] == role.name }
|
|
109
|
+
|
|
110
|
+
entry "member_orphan", member.host, role, "member #{member.id} is started but runs no #{role} container"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def started_members
|
|
116
|
+
@started_members ||= config.scaled_roles.flat_map(&:active_members)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def version_entries
|
|
120
|
+
return [] if @lock&.dig(:lock, :held)
|
|
121
|
+
|
|
122
|
+
config.roles.flat_map do |role|
|
|
123
|
+
by_host = running.transform_values { |containers| versions(containers.select { |container| container[:role] == role.name }) }.reject { |_, versions| versions.empty? }
|
|
124
|
+
|
|
125
|
+
multiple = by_host.select { |_, versions| versions.size > 1 }.map do |host, versions|
|
|
126
|
+
entry "multiple_running_versions", host, role, "#{role} runs #{versions.join(", ")} on #{host}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
mismatch = if by_host.values.uniq.size > 1
|
|
130
|
+
[ entry("version_mismatch", nil, role, "#{role} runs " + by_host.map { |host, versions| "#{versions.join("+")} on #{host}" }.join(", ")) ]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
multiple + mismatch.to_a
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def config
|
|
138
|
+
@config ||= DASH.config
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def running
|
|
142
|
+
@running ||= Array(@containers[:hosts]).reject { |host| host[:error] }.to_h do |host|
|
|
143
|
+
[ host[:host], Array(host[:containers]).select { |container| container[:state] == "running" } ]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def proxied_roles_on(host)
|
|
148
|
+
config.roles.select { |role| role.running_proxy? && role.hosts.include?(host) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# dash-proxy lists targets as "<container id or host>:<port>"; the id is the short one
|
|
152
|
+
# dash deployed with, the host is what the load balancer forwards to.
|
|
153
|
+
def targets(service)
|
|
154
|
+
return [] unless service
|
|
155
|
+
|
|
156
|
+
listed = TARGET_KEYS.flat_map { |key| Array(service[key]) }
|
|
157
|
+
listed = service["target"].to_s.split(",") if listed.empty?
|
|
158
|
+
listed.map { |target| target.strip.sub(/:\d+\z/, "") }.reject(&:empty?).uniq
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def versions(containers)
|
|
162
|
+
containers.map { |container| container[:version].to_s.sub(REPLACED_SUFFIX, "") }.uniq.sort
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def entry(code, host, role, detail)
|
|
166
|
+
{ code: code, host: host, role: role&.name, detail: detail }
|
|
167
|
+
end
|
|
168
|
+
end
|