kamal 2.10.1 → 2.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a154bea7b15565394f37d8cbbfc8860e3d6ef337526779390ebd96d917d2d7e
4
- data.tar.gz: a08c4cd341c5404cd9a5401b24918cdd32d498dbb778a655e1cb7f667030e519
3
+ metadata.gz: c8f7525b3ed804be2810d0af353c254b8c7ebf4d5cf5916dbfa97d5bf4615d54
4
+ data.tar.gz: adfac172b3e4b45bc00092317bde87395fcedecf107b30c8500ab58097ef22d8
5
5
  SHA512:
6
- metadata.gz: b09103e638d6d14d9605d3327928cf0343a80ff213e9e7f5f42d0c372c60ab78b13e30a983b1dc47e75cb17d7b705e94c98ea73077b8a2e1f87959f684e7a71d
7
- data.tar.gz: db80e53f84048c1ee6a5f2d3a010469578cbeedb94826b2393dcd4552f9e063c70b42242b3ea705c659c02a9cc87b7311c8e372d106148bea5baaa261cf7811d
6
+ metadata.gz: 05150f9253685ce18dd910cf809a99e51449b7452536e6c3f39c8e37d5ce3db4806bd8e198ed3e29aa872523eb8937db5b58faee036619f8e5b43d1084eebffc
7
+ data.tar.gz: 47e77a8d347c44aff4326f9927a0933c0240b5bfdb90f1b623e85ecdad8ba48c0e3443d43cd062b265aa29e2dbd47d778499be39a079d3cb93d8856cb0388108
data/README.md CHANGED
@@ -6,7 +6,7 @@ From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal
6
6
 
7
7
  ## Contributing to the documentation
8
8
 
9
- Please help us improve Kamal's documentation on the [the basecamp/kamal-site repository](https://github.com/basecamp/kamal-site).
9
+ Please help us improve Kamal's documentation on [the basecamp/kamal-site repository](https://github.com/basecamp/kamal-site).
10
10
 
11
11
  ## License
12
12
 
@@ -1,8 +1,8 @@
1
1
  class Kamal::Cli::Alias::Command < Thor::DynamicCommand
2
2
  def run(instance, args = [])
3
- if (_alias = KAMAL.config.aliases[name])
3
+ if (command = KAMAL.resolve_alias(name))
4
4
  KAMAL.reset
5
- Kamal::Cli::Main.start(Shellwords.split(_alias.command) + ARGV[1..-1])
5
+ Kamal::Cli::Main.start(Shellwords.split(command) + ARGV[1..-1])
6
6
  else
7
7
  super
8
8
  end
@@ -5,6 +5,8 @@ module Kamal::Cli
5
5
  class Base < Thor
6
6
  include SSHKit::DSL
7
7
 
8
+ VERBOSITY = { verbose: :debug, quiet: :error }.freeze
9
+
8
10
  def self.exit_on_failure?() true end
9
11
  def self.dynamic_command_class() Kamal::Cli::Alias::Command end
10
12
 
@@ -43,11 +45,11 @@ module Kamal::Cli
43
45
  KAMAL.tap do |commander|
44
46
  if options[:verbose]
45
47
  ENV["VERBOSE"] = "1" # For backtraces via cli/start
46
- commander.verbosity = :debug
48
+ commander.verbosity = VERBOSITY[:verbose]
47
49
  end
48
50
 
49
51
  if options[:quiet]
50
- commander.verbosity = :error
52
+ commander.verbosity = VERBOSITY[:quiet]
51
53
  end
52
54
 
53
55
  commander.configure \
@@ -141,10 +143,21 @@ module Kamal::Cli
141
143
  subcommand: subcommand
142
144
  }.compact
143
145
 
144
- say "Running the #{hook} hook...", :magenta
146
+ hooks_output = KAMAL.config.hooks_output_for(hook)
147
+
148
+ # CLI flags override config: -q hides all, -v shows all
149
+ # Config setting :verbose forces output, :quiet forces silence
150
+ hook_verbosity = if KAMAL.verbosity == :info && hooks_output
151
+ VERBOSITY.fetch(hooks_output)
152
+ else
153
+ KAMAL.verbosity
154
+ end
155
+
145
156
  with_env KAMAL.hook.env(**details, **extra_details) do
146
- run_locally do
147
- execute *KAMAL.hook.run(hook)
157
+ KAMAL.with_verbosity(hook_verbosity) do
158
+ run_locally do
159
+ execute *KAMAL.hook.run(hook)
160
+ end
148
161
  end
149
162
  rescue SSHKit::Command::Failed => e
150
163
  raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
@@ -160,7 +173,7 @@ module Kamal::Cli
160
173
 
161
174
  def pre_connect_if_required
162
175
  if !KAMAL.connected?
163
- run_hook "pre-connect"
176
+ run_hook "pre-connect", secrets: true unless options[:skip_hooks]
164
177
  KAMAL.connected = true
165
178
  end
166
179
  end
@@ -13,7 +13,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
13
13
  def push
14
14
  cli = self
15
15
 
16
- # Ensure pre-connect hooks run before the build, they may needed for a remote builder
16
+ # Ensure pre-connect hooks run before the build, they may be needed for a remote builder
17
17
  # or the pre-build hooks.
18
18
  pre_connect_if_required
19
19
 
@@ -35,6 +35,16 @@ class Kamal::Cli::Server < Kamal::Cli::Base
35
35
  if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
36
36
  info "Missing Docker on #{host}. Installing…"
37
37
  execute *KAMAL.docker.install
38
+
39
+ unless execute(*KAMAL.docker.root?, raise_on_non_zero_exit: false) ||
40
+ execute(*KAMAL.docker.in_docker_group?, raise_on_non_zero_exit: false)
41
+ execute *KAMAL.docker.add_to_docker_group
42
+ begin
43
+ execute *KAMAL.docker.refresh_session
44
+ rescue IOError
45
+ info "Session refreshed due to group change."
46
+ end
47
+ end
38
48
  else
39
49
  missing << host
40
50
  end
@@ -46,27 +46,19 @@ class Kamal::Commander
46
46
 
47
47
  def specific_roles=(role_names)
48
48
  @specifics = nil
49
- if role_names.present?
50
- @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
51
-
52
- if @specific_roles.empty?
53
- raise ArgumentError, "No --roles match for #{role_names.join(',')}"
54
- end
55
-
56
- @specific_roles
49
+ @specific_roles = if role_names.present?
50
+ filtered = Kamal::Utils.filter_specific_items(role_names, config.roles)
51
+ raise ArgumentError, "No --roles match for #{role_names.join(',')}" if filtered.empty?
52
+ filtered
57
53
  end
58
54
  end
59
55
 
60
56
  def specific_hosts=(hosts)
61
57
  @specifics = nil
62
- if hosts.present?
63
- @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
64
-
65
- if @specific_hosts.empty?
66
- raise ArgumentError, "No --hosts match for #{hosts.join(',')}"
67
- end
68
-
69
- @specific_hosts
58
+ @specific_hosts = if hosts.present?
59
+ filtered = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
60
+ raise ArgumentError, "No --hosts match for #{hosts.join(',')}" if filtered.empty?
61
+ filtered
70
62
  end
71
63
  end
72
64
 
@@ -129,6 +121,15 @@ class Kamal::Commander
129
121
  config.aliases[name]
130
122
  end
131
123
 
124
+ def resolve_alias(name)
125
+ if @config
126
+ @config.aliases[name]&.command
127
+ else
128
+ raw_config = Kamal::Configuration.load_raw_config(**@config_kwargs.to_h.slice(:config_file, :destination))
129
+ raw_config[:aliases]&.dig(name)
130
+ end
131
+ end
132
+
132
133
  def with_verbosity(level)
133
134
  old_level = self.verbosity
134
135
 
@@ -68,6 +68,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
68
68
  *network_args,
69
69
  *env_args,
70
70
  *volume_args,
71
+ *option_args,
71
72
  image,
72
73
  *command
73
74
  end
@@ -11,11 +11,11 @@ module Kamal::Commands
11
11
  end
12
12
 
13
13
  def run_over_ssh(*command, host:)
14
- "ssh#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
14
+ "ssh#{ssh_config_args}#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
15
15
  end
16
16
 
17
17
  def container_id_for(container_name:, only_running: false)
18
- docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
18
+ docker :container, :ls, *("--all" unless only_running), "--filter", "'name=^#{container_name}$'", "--quiet"
19
19
  end
20
20
 
21
21
  def make_directory_for(remote_file)
@@ -100,6 +100,19 @@ module Kamal::Commands
100
100
  Kamal::Tags.from_config(config, **details)
101
101
  end
102
102
 
103
+ def ssh_config_args
104
+ case config.ssh.config
105
+ when Array
106
+ config.ssh.config.map { |file| " -F #{file}" }.join
107
+ when String
108
+ " -F #{config.ssh.config}"
109
+ when true
110
+ "" # Use default SSH config
111
+ when false
112
+ " -F /dev/null" # Ignore SSH config
113
+ end
114
+ end
115
+
103
116
  def ssh_proxy_args
104
117
  case config.ssh.proxy
105
118
  when Net::SSH::Proxy::Jump
@@ -16,7 +16,23 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
16
16
 
17
17
  # Do we have superuser access to install Docker and start system services?
18
18
  def superuser?
19
- [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
19
+ [ '[ "${EUID:-$(id -u)}" -eq 0 ] || sudo -nl usermod >/dev/null' ]
20
+ end
21
+
22
+ def root?
23
+ [ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
24
+ end
25
+
26
+ def in_docker_group?
27
+ [ 'id -nG "${USER:-$(id -un)}" | grep -qw docker' ]
28
+ end
29
+
30
+ def add_to_docker_group
31
+ [ 'sudo -n usermod -aG docker "${USER:-$(id -un)}"' ]
32
+ end
33
+
34
+ def refresh_session
35
+ [ "kill -HUP $PPID" ]
20
36
  end
21
37
 
22
38
  def create_network
@@ -37,7 +37,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
37
37
  end
38
38
 
39
39
  def info
40
- docker :ps, "--filter", "name=^#{container_name}$"
40
+ docker :ps, "--filter", "'name=^#{container_name}$'"
41
41
  end
42
42
 
43
43
  def version
@@ -24,3 +24,6 @@ aliases:
24
24
  # Each alias is named and can only contain lowercase letters, numbers, dashes, and underscores:
25
25
  aliases:
26
26
  uname: app exec -p -q -r web "uname -a"
27
+ #
28
+ # Aliases can include a destination with the `-d` flag:
29
+ staging_deploy: deploy -d staging
@@ -85,6 +85,26 @@ asset_path: /path/to/assets
85
85
  # See https://kamal-deploy.org/docs/hooks for more information:
86
86
  hooks_path: /user_home/kamal/hooks
87
87
 
88
+ # Hook output
89
+ #
90
+ # Hook output visibility. Can be set globally or per-hook.
91
+ # CLI flags (`-v`, `-q`) override these settings.
92
+ #
93
+ # - `:quiet` - hook output is hidden
94
+ # - `:verbose` - hook output is shown
95
+ #
96
+ # With no setting, hook output follows CLI verbosity flags.
97
+ #
98
+ # Note: Failed hooks always show output in the error message regardless of setting.
99
+ #
100
+ # Global setting for all hooks:
101
+ hooks_output: :verbose
102
+
103
+ # Or per-hook settings:
104
+ hooks_output:
105
+ pre-deploy: :verbose
106
+ pre-build: :quiet
107
+
88
108
  # Secrets path
89
109
  #
90
110
  # Path to secrets, defaults to `.kamal/secrets`.
@@ -1,5 +1,5 @@
1
1
  class Kamal::Configuration::Proxy::Run
2
- MINIMUM_VERSION = "v0.9.0"
2
+ MINIMUM_VERSION = "v0.9.2"
3
3
  DEFAULT_HTTP_PORT = 80
4
4
  DEFAULT_HTTPS_PORT = 443
5
5
  DEFAULT_LOG_MAX_SIZE = "10m"
@@ -29,6 +29,8 @@ class Kamal::Configuration::Validator
29
29
  end
30
30
  elsif key.to_s == "ssl"
31
31
  validate_type! value, TrueClass, FalseClass, Hash
32
+ elsif key.to_s == "hooks_output"
33
+ validate_hooks_output!(value)
32
34
  elsif key == "hosts"
33
35
  validate_servers! value
34
36
  elsif example_value.is_a?(Array)
@@ -161,6 +163,19 @@ class Kamal::Configuration::Validator
161
163
  end
162
164
  end
163
165
 
166
+ def validate_hooks_output!(value)
167
+ # hooks_output can be either a symbol/string (global) or a hash (per-hook)
168
+ if value.is_a?(Hash)
169
+ value.each do |hook, level|
170
+ with_context(hook) do
171
+ validate_type! level, String, Symbol
172
+ end
173
+ end
174
+ else
175
+ validate_type! value, String, Symbol
176
+ end
177
+ end
178
+
164
179
  def validate_type!(value, *types)
165
180
  type_error(*types) unless types.any? { |type| valid_type?(value, type) }
166
181
  end
@@ -6,6 +6,8 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Kamal::Configuration
9
+ HOOKS_OUTPUT_LEVELS = [ :quiet, :verbose ].freeze
10
+
9
11
  delegate :service, :labels, :hooks_path, to: :raw_config, allow_nil: true
10
12
  delegate :argumentize, :optionize, to: Kamal::Utils
11
13
 
@@ -18,11 +20,15 @@ class Kamal::Configuration
18
20
  def create_from(config_file:, destination: nil, version: nil)
19
21
  ENV["KAMAL_DESTINATION"] = destination
20
22
 
21
- raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
23
+ raw_config = load_raw_config(config_file: config_file, destination: destination)
22
24
 
23
25
  new raw_config, destination: destination, version: version
24
26
  end
25
27
 
28
+ def load_raw_config(config_file:, destination: nil)
29
+ load_config_files(config_file, *destination_config_file(config_file, destination))
30
+ end
31
+
26
32
  private
27
33
  def load_config_files(*files)
28
34
  files.inject({}) { |config, file| config.deep_merge! load_config_file(file) }
@@ -32,7 +38,9 @@ class Kamal::Configuration
32
38
  if file.exist?
33
39
  # Newer Psych doesn't load aliases by default
34
40
  load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
35
- YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
41
+ template = File.read(file)
42
+ rendered = ERB.new(template, trim_mode: "-").result
43
+ YAML.send(load_method, rendered).symbolize_keys
36
44
  else
37
45
  raise "Configuration file not found in #{file}"
38
46
  end
@@ -78,6 +86,7 @@ class Kamal::Configuration
78
86
  ensure_unique_hosts_for_ssl_roles
79
87
  ensure_local_registry_remote_builder_has_ssh_url
80
88
  ensure_no_conflicting_proxy_runs
89
+ ensure_valid_hooks_output!
81
90
  end
82
91
 
83
92
  def version=(version)
@@ -279,6 +288,15 @@ class Kamal::Configuration
279
288
  env_tags.detect { |t| t.name == name.to_s }
280
289
  end
281
290
 
291
+ def hooks_output_for(hook)
292
+ case raw_config.hooks_output
293
+ when Symbol, String
294
+ raw_config.hooks_output.to_sym
295
+ when Hash
296
+ raw_config.hooks_output[hook]&.to_sym
297
+ end
298
+ end
299
+
282
300
  def to_h
283
301
  {
284
302
  roles: role_names,
@@ -409,6 +427,21 @@ class Kamal::Configuration
409
427
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
410
428
  end
411
429
 
430
+ def ensure_valid_hooks_output!
431
+ case raw_config.hooks_output
432
+ when Symbol, String
433
+ validate_hooks_output_level!(raw_config.hooks_output.to_sym)
434
+ when Hash
435
+ raw_config.hooks_output.each { |hook, level| validate_hooks_output_level!(level.to_sym, hook) }
436
+ end
437
+ end
438
+
439
+ def validate_hooks_output_level!(level, hook = nil)
440
+ return if HOOKS_OUTPUT_LEVELS.include?(level)
441
+ context = hook ? " for hook '#{hook}'" : ""
442
+ raise Kamal::ConfigurationError, "Invalid hooks_output '#{level}'#{context}, must be one of: #{HOOKS_OUTPUT_LEVELS.join(', ')}"
443
+ end
444
+
412
445
  def git_version
413
446
  @git_version ||=
414
447
  if Kamal::Git.used?
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.10.1"
2
+ VERSION = "2.11.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.10.1
4
+ version: 2.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -175,6 +175,20 @@ dependencies:
175
175
  - - ">="
176
176
  - !ruby/object:Gem::Version
177
177
  version: '0'
178
+ - !ruby/object:Gem::Dependency
179
+ name: minitest
180
+ requirement: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - "<"
183
+ - !ruby/object:Gem::Version
184
+ version: '6'
185
+ type: :development
186
+ prerelease: false
187
+ version_requirements: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - "<"
190
+ - !ruby/object:Gem::Version
191
+ version: '6'
178
192
  - !ruby/object:Gem::Dependency
179
193
  name: mocha
180
194
  requirement: !ruby/object:Gem::Requirement