Class: Dash::Commands::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/dash/commands/base.rb

Constant Summary collapse

NO_HEALTHCHECK =

Prefixed onto the docker state when the container declares no healthcheck at all, so "nothing is probing this container" stays distinguishable from "the probe passed" — both used to reach the poller as running. The state is kept after the prefix so a healthcheck-less container that died still reports why.

"no-healthcheck"
DOCKER_HEALTH_STATUS_FORMAT =
"'{{if .State.Health}}{{.State.Health.Status}}{{else}}#{NO_HEALTHCHECK}:{{.State.Status}}{{end}}'"
READY_STATUSES =

The statuses a boot accepts as ready. Dash::Cli::Healthcheck::Poller decides what a status means; Dash::Commands::App#wait_for_ready only decides when to stop looking, and it stops on exactly these. The two must agree: a status the host loop returned early for that the poller would not accept fails a boot the old client-side poll would have waited out.

[ "healthy", "#{NO_HEALTHCHECK}:running" ].freeze
EXEC_PROBE_FAILED =

What a healthcheck: exec: probe reports when it exits non-zero. Produced by the host-side wait, read back by the poller, so it is a wire format, not a message.

"exec probe exited non-zero"
READINESS_PROGRESS_PREFIX =

The line #wait_for_ready prints to stderr on every attempt, read back by Dash::Cli::Healthcheck::ProgressReporter. stderr, because a capture returns stdout alone - which keeps the captured value the final status and nothing else.

"dash-readiness"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Base

Returns a new instance of Base.



31
32
33
# File 'lib/dash/commands/base.rb', line 31

def initialize(config)
  @config = config
end

Instance Attribute Details

#config ⇒ Object

Returns the value of attribute config.



29
30
31
# File 'lib/dash/commands/base.rb', line 29

def config
  @config
end

Instance Method Details

#confirmed_empty?(list_command) ⇒ Boolean

True only when list_command's own output is confirmed empty - never inferred from a failure. docker container inspect name > /dev/null 2>&1 (negated) cannot tell "no such container" from "the daemon could not be asked" - both exit non-zero - so a transient failure there reads as confirmed absence. A list exits 0 whichever way the match went and non-zero only on a genuine failure, so result=$(list) && [ -z "$result" ] fails closed: result=$(list) carries list's own exit status (POSIX; verified against sh and bash), so a failed list stops the chain before the test runs. list_command must be a listing (docker container/volume ls), never an inspect.

Returns:

  • (Boolean)


51
52
53
# File 'lib/dash/commands/base.rb', line 51

def confirmed_empty?(list_command)
  [ "result=$(#{list_command.join(" ")})", "&&", "[", "-z", "\"$result\"", "]" ]
end

#container_id_for(container_name:, only_running: false) ⇒ Object



39
40
41
# File 'lib/dash/commands/base.rb', line 39

def container_id_for(container_name:, only_running: false)
  docker :container, :ls, *("--all" unless only_running), "--filter", "'name=^#{container_name}$'", "--quiet"
end

#ensure_docker_installed ⇒ Object



101
102
103
104
105
# File 'lib/dash/commands/base.rb', line 101

def ensure_docker_installed
  combine \
    ensure_local_docker_installed,
    ensure_local_buildx_installed
end

#ensure_run_directory ⇒ Object

Creates the run directory, renaming a pre-3b .kamal into place first.

Lives on Base because two unlocked paths reach the run directory before Dash::Cli::Base#ensure_run_directory does — the auditor records "Pulled image" during build:pull, and every mkdir -p .dash/... underneath it creates the parent. If any of them made the directory with a bare mkdir, .dash would exist by the time the guard ran and the legacy tree would be stranded. Everything that can be first must run the same command.

The migration is safe to repeat on every command:

  • Idempotent - once .dash exists the second guard is false forever.
  • Atomic - the two are siblings in the SSH user's home, so this is a rename within one filesystem, never a copy.
  • Invisible to running containers - a bind mount resolves to an inode, so the live proxy keeps serving from the renamed directory and only picks up the new path when it is next recreated.

test leads deliberately: SSHKit's command map passes if/test/time/ exec through untouched and prefixes everything else with /usr/bin/env. The trailing || true keeps the exit status zero when there is nothing to migrate, since callers execute this with raise_on_non_zero_exit on.



85
86
87
# File 'lib/dash/commands/base.rb', line 85

def ensure_run_directory
  combine migrate_legacy_run_directory, make_directory(config.run_directory)
end

#make_directory(path) ⇒ Object



59
60
61
# File 'lib/dash/commands/base.rb', line 59

def make_directory(path)
  [ :mkdir, "-p", path ]
end

#make_directory_for(remote_file) ⇒ Object



55
56
57
# File 'lib/dash/commands/base.rb', line 55

def make_directory_for(remote_file)
  make_directory Pathname.new(remote_file).dirname.to_s
end

#read_file(file, default: nil) ⇒ Object



97
98
99
# File 'lib/dash/commands/base.rb', line 97

def read_file(file, default: nil)
  combine [ :cat, file, "2>", "/dev/null" ], [ :echo, "\"#{default}\"" ], by: "||"
end

#remove_directory(path) ⇒ Object



89
90
91
# File 'lib/dash/commands/base.rb', line 89

def remove_directory(path)
  [ :rm, "-r", path ]
end

#remove_file(path) ⇒ Object



93
94
95
# File 'lib/dash/commands/base.rb', line 93

def remove_file(path)
  [ :rm, path ]
end

#run_over_ssh(*command, host:) ⇒ Object



35
36
37
# File 'lib/dash/commands/base.rb', line 35

def run_over_ssh(*command, host:)
  "ssh#{ssh_config_args}#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
end