kamal 1.8.3 → 2.7.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +92 -38
  4. data/lib/kamal/cli/alias/command.rb +10 -0
  5. data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
  6. data/lib/kamal/cli/app/boot.rb +23 -16
  7. data/lib/kamal/cli/app/error_pages.rb +33 -0
  8. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  9. data/lib/kamal/cli/app.rb +132 -30
  10. data/lib/kamal/cli/base.rb +57 -53
  11. data/lib/kamal/cli/build.rb +81 -38
  12. data/lib/kamal/cli/healthcheck/barrier.rb +2 -0
  13. data/lib/kamal/cli/healthcheck/poller.rb +18 -39
  14. data/lib/kamal/cli/lock.rb +2 -3
  15. data/lib/kamal/cli/main.rb +60 -59
  16. data/lib/kamal/cli/proxy.rb +290 -0
  17. data/lib/kamal/cli/prune.rb +0 -1
  18. data/lib/kamal/cli/registry.rb +2 -0
  19. data/lib/kamal/cli/secrets.rb +49 -0
  20. data/lib/kamal/cli/server.rb +6 -5
  21. data/lib/kamal/cli/templates/deploy.yml +53 -53
  22. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +2 -12
  23. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  24. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  25. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  26. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  28. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
  29. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +19 -6
  30. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  31. data/lib/kamal/cli/templates/secrets +17 -0
  32. data/lib/kamal/cli.rb +2 -0
  33. data/lib/kamal/commander/specifics.rb +19 -6
  34. data/lib/kamal/commander.rb +39 -32
  35. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  36. data/lib/kamal/commands/accessory.rb +19 -19
  37. data/lib/kamal/commands/app/assets.rb +10 -10
  38. data/lib/kamal/commands/app/containers.rb +2 -2
  39. data/lib/kamal/commands/app/error_pages.rb +9 -0
  40. data/lib/kamal/commands/app/execution.rb +7 -4
  41. data/lib/kamal/commands/app/images.rb +1 -1
  42. data/lib/kamal/commands/app/logging.rb +16 -6
  43. data/lib/kamal/commands/app/proxy.rb +32 -0
  44. data/lib/kamal/commands/app.rb +25 -24
  45. data/lib/kamal/commands/auditor.rb +12 -3
  46. data/lib/kamal/commands/base.rb +54 -8
  47. data/lib/kamal/commands/builder/base.rb +46 -16
  48. data/lib/kamal/commands/builder/clone.rb +16 -14
  49. data/lib/kamal/commands/builder/cloud.rb +22 -0
  50. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  51. data/lib/kamal/commands/builder/local.rb +14 -0
  52. data/lib/kamal/commands/builder/pack.rb +46 -0
  53. data/lib/kamal/commands/builder/remote.rb +63 -0
  54. data/lib/kamal/commands/builder.rb +21 -45
  55. data/lib/kamal/commands/docker.rb +4 -0
  56. data/lib/kamal/commands/hook.rb +8 -2
  57. data/lib/kamal/commands/lock.rb +2 -6
  58. data/lib/kamal/commands/proxy.rb +127 -0
  59. data/lib/kamal/commands/prune.rb +1 -9
  60. data/lib/kamal/commands/registry.rb +9 -7
  61. data/lib/kamal/commands/server.rb +11 -1
  62. data/lib/kamal/configuration/accessory.rb +89 -12
  63. data/lib/kamal/configuration/alias.rb +15 -0
  64. data/lib/kamal/configuration/builder.rb +73 -15
  65. data/lib/kamal/configuration/docs/accessory.yml +53 -15
  66. data/lib/kamal/configuration/docs/alias.yml +26 -0
  67. data/lib/kamal/configuration/docs/boot.yml +3 -3
  68. data/lib/kamal/configuration/docs/builder.yml +63 -38
  69. data/lib/kamal/configuration/docs/configuration.yml +62 -46
  70. data/lib/kamal/configuration/docs/env.yml +61 -17
  71. data/lib/kamal/configuration/docs/logging.yml +3 -3
  72. data/lib/kamal/configuration/docs/proxy.yml +168 -0
  73. data/lib/kamal/configuration/docs/registry.yml +20 -13
  74. data/lib/kamal/configuration/docs/role.yml +14 -13
  75. data/lib/kamal/configuration/docs/servers.yml +2 -2
  76. data/lib/kamal/configuration/docs/ssh.yml +23 -19
  77. data/lib/kamal/configuration/docs/sshkit.yml +4 -4
  78. data/lib/kamal/configuration/env/tag.rb +4 -3
  79. data/lib/kamal/configuration/env.rb +19 -17
  80. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  81. data/lib/kamal/configuration/proxy.rb +124 -0
  82. data/lib/kamal/configuration/registry.rb +7 -6
  83. data/lib/kamal/configuration/role.rb +69 -98
  84. data/lib/kamal/configuration/servers.rb +8 -1
  85. data/lib/kamal/configuration/validator/accessory.rb +6 -2
  86. data/lib/kamal/configuration/validator/alias.rb +15 -0
  87. data/lib/kamal/configuration/validator/builder.rb +6 -0
  88. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  89. data/lib/kamal/configuration/validator/role.rb +3 -1
  90. data/lib/kamal/configuration/validator/servers.rb +1 -1
  91. data/lib/kamal/configuration/validator.rb +62 -24
  92. data/lib/kamal/configuration.rb +96 -50
  93. data/lib/kamal/docker.rb +30 -0
  94. data/lib/kamal/env_file.rb +7 -1
  95. data/lib/kamal/git.rb +10 -0
  96. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  97. data/lib/kamal/secrets/adapters/base.rb +33 -0
  98. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  99. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  100. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  101. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  102. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  103. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  104. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  105. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  106. data/lib/kamal/secrets/adapters/test.rb +14 -0
  107. data/lib/kamal/secrets/adapters.rb +16 -0
  108. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  109. data/lib/kamal/secrets.rb +42 -0
  110. data/lib/kamal/sshkit_with_ext.rb +1 -0
  111. data/lib/kamal/utils.rb +30 -0
  112. data/lib/kamal/version.rb +1 -1
  113. data/lib/kamal.rb +3 -1
  114. metadata +63 -36
  115. data/lib/kamal/cli/env.rb +0 -54
  116. data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
  117. data/lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample +0 -3
  118. data/lib/kamal/cli/templates/template.env +0 -2
  119. data/lib/kamal/cli/traefik.rb +0 -122
  120. data/lib/kamal/commands/app/cord.rb +0 -22
  121. data/lib/kamal/commands/builder/multiarch/remote.rb +0 -65
  122. data/lib/kamal/commands/builder/multiarch.rb +0 -41
  123. data/lib/kamal/commands/builder/native/cached.rb +0 -25
  124. data/lib/kamal/commands/builder/native/remote.rb +0 -67
  125. data/lib/kamal/commands/builder/native.rb +0 -20
  126. data/lib/kamal/commands/traefik.rb +0 -85
  127. data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
  128. data/lib/kamal/configuration/docs/traefik.yml +0 -62
  129. data/lib/kamal/configuration/healthcheck.rb +0 -63
  130. data/lib/kamal/configuration/traefik.rb +0 -60
@@ -0,0 +1,290 @@
1
+ class Kamal::Cli::Proxy < Kamal::Cli::Base
2
+ desc "boot", "Boot proxy on servers"
3
+ def boot
4
+ with_lock do
5
+ on(KAMAL.hosts) do |host|
6
+ execute *KAMAL.docker.create_network
7
+ rescue SSHKit::Command::Failed => e
8
+ raise unless e.message.include?("already exists")
9
+ end
10
+
11
+ on(KAMAL.proxy_hosts) do |host|
12
+ execute *KAMAL.registry.login
13
+
14
+ version = capture_with_info(*KAMAL.proxy.version).strip.presence
15
+
16
+ if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION)
17
+ raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}"
18
+ end
19
+ execute *KAMAL.proxy.ensure_apps_config_directory
20
+ execute *KAMAL.proxy.start_or_run
21
+ end
22
+ end
23
+ end
24
+
25
+ desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
26
+ option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
27
+ option :publish_host_ip, type: :string, repeatable: true, default: nil, desc: "Host IP address to bind HTTP/HTTPS traffic to. Defaults to all interfaces"
28
+ option :http_port, type: :numeric, default: Kamal::Configuration::Proxy::Boot::DEFAULT_HTTP_PORT, desc: "HTTP port to publish on the host"
29
+ option :https_port, type: :numeric, default: Kamal::Configuration::Proxy::Boot::DEFAULT_HTTPS_PORT, desc: "HTTPS port to publish on the host"
30
+ option :log_max_size, type: :string, default: Kamal::Configuration::Proxy::Boot::DEFAULT_LOG_MAX_SIZE, desc: "Max size of proxy logs"
31
+ option :registry, type: :string, default: nil, desc: "Registry to use for the proxy image"
32
+ option :repository, type: :string, default: nil, desc: "Repository for the proxy image"
33
+ option :image_version, type: :string, default: nil, desc: "Version of the proxy to run"
34
+ option :metrics_port, type: :numeric, default: nil, desc: "Port to report prometheus metrics on"
35
+ option :debug, type: :boolean, default: false, desc: "Whether to run the proxy in debug mode"
36
+ option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
37
+ def boot_config(subcommand)
38
+ proxy_boot_config = KAMAL.config.proxy_boot
39
+
40
+ case subcommand
41
+ when "set"
42
+ boot_options = [
43
+ *(proxy_boot_config.publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]),
44
+ *(proxy_boot_config.logging_args(options[:log_max_size])),
45
+ *("--expose=#{options[:metrics_port]}" if options[:metrics_port]),
46
+ *options[:docker_options].map { |option| "--#{option}" }
47
+ ]
48
+
49
+ image = [
50
+ options[:registry].presence,
51
+ options[:repository].presence || proxy_boot_config.repository_name,
52
+ proxy_boot_config.image_name
53
+ ].compact.join("/")
54
+
55
+ image_version = options[:image_version]
56
+
57
+ run_command_options = { debug: options[:debug] || nil, "metrics-port": options[:metrics_port] }.compact
58
+ run_command = "kamal-proxy run #{Kamal::Utils.optionize(run_command_options).join(" ")}" if run_command_options.any?
59
+
60
+ on(KAMAL.proxy_hosts) do |host|
61
+ execute(*KAMAL.proxy.ensure_proxy_directory)
62
+ if boot_options != proxy_boot_config.default_boot_options
63
+ upload! StringIO.new(boot_options.join(" ")), proxy_boot_config.options_file
64
+ else
65
+ execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false
66
+ end
67
+
68
+ if image != proxy_boot_config.image_default
69
+ upload! StringIO.new(image), proxy_boot_config.image_file
70
+ else
71
+ execute *KAMAL.proxy.reset_image, raise_on_non_zero_exit: false
72
+ end
73
+
74
+ if image_version
75
+ upload! StringIO.new(image_version), proxy_boot_config.image_version_file
76
+ else
77
+ execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false
78
+ end
79
+
80
+ if run_command
81
+ upload! StringIO.new(run_command), proxy_boot_config.run_command_file
82
+ else
83
+ execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false
84
+ end
85
+ end
86
+ when "get"
87
+
88
+ on(KAMAL.proxy_hosts) do |host|
89
+ puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.boot_config)}"
90
+ end
91
+ when "reset"
92
+ on(KAMAL.proxy_hosts) do |host|
93
+ execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false
94
+ execute *KAMAL.proxy.reset_image, raise_on_non_zero_exit: false
95
+ execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false
96
+ execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false
97
+ end
98
+ else
99
+ raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
100
+ end
101
+ end
102
+
103
+ desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
104
+ option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
105
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
106
+ def reboot
107
+ confirming "This will cause a brief outage on each host. Are you sure?" do
108
+ with_lock do
109
+ host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ]
110
+ host_groups.each do |hosts|
111
+ host_list = Array(hosts).join(",")
112
+ run_hook "pre-proxy-reboot", hosts: host_list
113
+ on(hosts) do |host|
114
+ execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
115
+ execute *KAMAL.registry.login
116
+
117
+ "Stopping and removing kamal-proxy on #{host}, if running..."
118
+ execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
119
+ execute *KAMAL.proxy.remove_container
120
+ execute *KAMAL.proxy.ensure_apps_config_directory
121
+
122
+ execute *KAMAL.proxy.run
123
+ end
124
+ run_hook "post-proxy-reboot", hosts: host_list
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)", hide: true
131
+ option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
132
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
133
+ def upgrade
134
+ invoke_options = { "version" => KAMAL.config.latest_tag }.merge(options)
135
+
136
+ confirming "This will cause a brief outage on each host. Are you sure?" do
137
+ host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ]
138
+ host_groups.each do |hosts|
139
+ host_list = Array(hosts).join(",")
140
+ say "Upgrading proxy on #{host_list}...", :magenta
141
+ run_hook "pre-proxy-reboot", hosts: host_list
142
+ on(hosts) do |host|
143
+ execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
144
+ execute *KAMAL.registry.login
145
+
146
+ "Stopping and removing Traefik on #{host}, if running..."
147
+ execute *KAMAL.proxy.cleanup_traefik
148
+
149
+ "Stopping and removing kamal-proxy on #{host}, if running..."
150
+ execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
151
+ execute *KAMAL.proxy.remove_container
152
+ execute *KAMAL.proxy.remove_image
153
+ end
154
+
155
+ KAMAL.with_specific_hosts(hosts) do
156
+ invoke "kamal:cli:proxy:boot", [], invoke_options
157
+ reset_invocation(Kamal::Cli::Proxy)
158
+ invoke "kamal:cli:app:boot", [], invoke_options
159
+ reset_invocation(Kamal::Cli::App)
160
+ invoke "kamal:cli:prune:all", [], invoke_options
161
+ reset_invocation(Kamal::Cli::Prune)
162
+ end
163
+
164
+ run_hook "post-proxy-reboot", hosts: host_list
165
+ say "Upgraded proxy on #{host_list}", :magenta
166
+ end
167
+ end
168
+ end
169
+
170
+ desc "start", "Start existing proxy container on servers"
171
+ def start
172
+ with_lock do
173
+ on(KAMAL.proxy_hosts) do |host|
174
+ execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
175
+ execute *KAMAL.proxy.start
176
+ end
177
+ end
178
+ end
179
+
180
+ desc "stop", "Stop existing proxy container on servers"
181
+ def stop
182
+ with_lock do
183
+ on(KAMAL.proxy_hosts) do |host|
184
+ execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
185
+ execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
186
+ end
187
+ end
188
+ end
189
+
190
+ desc "restart", "Restart existing proxy container on servers"
191
+ def restart
192
+ with_lock do
193
+ stop
194
+ start
195
+ end
196
+ end
197
+
198
+ desc "details", "Show details about proxy container from servers"
199
+ def details
200
+ on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy" }
201
+ end
202
+
203
+ desc "logs", "Show log lines from proxy on servers"
204
+ option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
205
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
206
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
207
+ option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
208
+ option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
209
+ def logs
210
+ grep = options[:grep]
211
+ timestamps = !options[:skip_timestamps]
212
+
213
+ if options[:follow]
214
+ run_locally do
215
+ info "Following logs on #{KAMAL.primary_host}..."
216
+ info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
217
+ exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
218
+ end
219
+ else
220
+ since = options[:since]
221
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
222
+
223
+ on(KAMAL.proxy_hosts) do |host|
224
+ puts_by_host host, capture(*KAMAL.proxy.logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
225
+ end
226
+ end
227
+ end
228
+
229
+ desc "remove", "Remove proxy container and image from servers"
230
+ option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
231
+ def remove
232
+ with_lock do
233
+ if removal_allowed?(options[:force])
234
+ stop
235
+ remove_container
236
+ remove_image
237
+ remove_proxy_directory
238
+ end
239
+ end
240
+ end
241
+
242
+ desc "remove_container", "Remove proxy container from servers", hide: true
243
+ def remove_container
244
+ with_lock do
245
+ on(KAMAL.proxy_hosts) do
246
+ execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
247
+ execute *KAMAL.proxy.remove_container
248
+ end
249
+ end
250
+ end
251
+
252
+ desc "remove_image", "Remove proxy image from servers", hide: true
253
+ def remove_image
254
+ with_lock do
255
+ on(KAMAL.proxy_hosts) do
256
+ execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
257
+ execute *KAMAL.proxy.remove_image
258
+ end
259
+ end
260
+ end
261
+
262
+ desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
263
+ def remove_proxy_directory
264
+ with_lock do
265
+ on(KAMAL.proxy_hosts) do
266
+ execute *KAMAL.proxy.remove_proxy_directory, raise_on_non_zero_exit: false
267
+ end
268
+ end
269
+ end
270
+
271
+ private
272
+ def removal_allowed?(force)
273
+ on(KAMAL.proxy_hosts) do |host|
274
+ app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i
275
+ raise "The are other applications installed on #{host}" if app_count > 0
276
+ end
277
+
278
+ true
279
+ rescue SSHKit::Runner::ExecuteError => e
280
+ raise unless e.message.include?("The are other applications installed on")
281
+
282
+ if force
283
+ say "Forcing, so removing the proxy, even though other apps are installed", :magenta
284
+ else
285
+ say "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", :magenta
286
+ end
287
+
288
+ force
289
+ end
290
+ end
@@ -28,7 +28,6 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
28
28
  on(KAMAL.hosts) do
29
29
  execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
30
30
  execute *KAMAL.prune.app_containers(retain: retain)
31
- execute *KAMAL.prune.healthcheck_containers
32
31
  end
33
32
  end
34
33
  end
@@ -3,6 +3,8 @@ class Kamal::Cli::Registry < Kamal::Cli::Base
3
3
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
4
4
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
5
5
  def login
6
+ ensure_docker_installed unless options[:skip_local]
7
+
6
8
  run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
7
9
  on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
8
10
  end
@@ -0,0 +1,49 @@
1
+ class Kamal::Cli::Secrets < Kamal::Cli::Base
2
+ desc "fetch [SECRETS...]", "Fetch secrets from a vault"
3
+ option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
4
+ option :account, type: :string, required: false, desc: "The account identifier or username"
5
+ option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
6
+ option :inline, type: :boolean, required: false, hidden: true
7
+ def fetch(*secrets)
8
+ adapter = initialize_adapter(options[:adapter])
9
+
10
+ if adapter.requires_account? && options[:account].blank?
11
+ return puts "No value provided for required options '--account'"
12
+ end
13
+
14
+ results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
15
+
16
+ return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
17
+ end
18
+
19
+ desc "extract", "Extract a single secret from the results of a fetch call"
20
+ option :inline, type: :boolean, required: false, hidden: true
21
+ def extract(name, secrets)
22
+ parsed_secrets = JSON.parse(secrets)
23
+ value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
24
+
25
+ raise "Could not find secret #{name}" if value.nil?
26
+
27
+ return_or_puts value, inline: options[:inline]
28
+ end
29
+
30
+ desc "print", "Print the secrets (for debugging)"
31
+ def print
32
+ KAMAL.config.secrets.to_h.each do |key, value|
33
+ puts "#{key}=#{value}"
34
+ end
35
+ end
36
+
37
+ private
38
+ def initialize_adapter(adapter)
39
+ Kamal::Secrets::Adapters.lookup(adapter)
40
+ end
41
+
42
+ def return_or_puts(value, inline: nil)
43
+ if inline
44
+ value
45
+ else
46
+ puts value
47
+ end
48
+ end
49
+ end
@@ -1,8 +1,11 @@
1
1
  class Kamal::Cli::Server < Kamal::Cli::Base
2
2
  desc "exec", "Run a custom command on the server (use --help to show options)"
3
3
  option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
4
- def exec(cmd)
5
- hosts = KAMAL.hosts | KAMAL.accessory_hosts
4
+ def exec(*cmd)
5
+ pre_connect_if_required
6
+
7
+ cmd = Kamal::Utils.join_commands(cmd)
8
+ hosts = KAMAL.hosts
6
9
 
7
10
  case
8
11
  when options[:interactive]
@@ -26,7 +29,7 @@ class Kamal::Cli::Server < Kamal::Cli::Base
26
29
  with_lock do
27
30
  missing = []
28
31
 
29
- on(KAMAL.hosts | KAMAL.accessory_hosts) do |host|
32
+ on(KAMAL.hosts) do |host|
30
33
  unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
31
34
  if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
32
35
  info "Missing Docker on #{host}. Installing…"
@@ -35,8 +38,6 @@ class Kamal::Cli::Server < Kamal::Cli::Base
35
38
  missing << host
36
39
  end
37
40
  end
38
-
39
- execute(*KAMAL.server.ensure_run_directory)
40
41
  end
41
42
 
42
43
  if missing.any?
@@ -2,11 +2,26 @@
2
2
  service: my-app
3
3
 
4
4
  # Name of the container image.
5
- image: user/my-app
5
+ image: my-user/my-app
6
6
 
7
7
  # Deploy to these servers.
8
8
  servers:
9
- - 192.168.0.1
9
+ web:
10
+ - 192.168.0.1
11
+ # job:
12
+ # hosts:
13
+ # - 192.168.0.1
14
+ # cmd: bin/jobs
15
+
16
+ # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
17
+ # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
18
+ #
19
+ # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20
+ proxy:
21
+ ssl: true
22
+ host: app.example.com
23
+ # Proxy connects to your container on port 80 by default.
24
+ # app_port: 3000
10
25
 
11
26
  # Credentials for your image host.
12
27
  registry:
@@ -14,33 +29,55 @@ registry:
14
29
  # server: registry.digitalocean.com / ghcr.io / ...
15
30
  username: my-user
16
31
 
17
- # Always use an access token rather than real password when possible.
32
+ # Always use an access token rather than real password (pulled from .kamal/secrets).
18
33
  password:
19
34
  - KAMAL_REGISTRY_PASSWORD
20
35
 
21
- # Inject ENV variables into containers (secrets come from .env).
22
- # Remember to run `kamal env push` after making changes!
36
+ # Configure builder setup.
37
+ builder:
38
+ arch: amd64
39
+ # Pass in additional build args needed for your Dockerfile.
40
+ # args:
41
+ # RUBY_VERSION: <%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %>
42
+
43
+ # Inject ENV variables into containers (secrets come from .kamal/secrets).
44
+ #
23
45
  # env:
24
46
  # clear:
25
47
  # DB_HOST: 192.168.0.2
26
48
  # secret:
27
49
  # - RAILS_MASTER_KEY
28
50
 
51
+ # Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
52
+ # "bin/kamal app logs -r job" will tail logs from the first server in the job section.
53
+ #
54
+ # aliases:
55
+ # shell: app exec --interactive --reuse "bash"
56
+
29
57
  # Use a different ssh user than root
58
+ #
30
59
  # ssh:
31
60
  # user: app
32
61
 
33
- # Configure builder setup.
34
- # builder:
35
- # args:
36
- # RUBY_VERSION: 3.2.0
37
- # secrets:
38
- # - GITHUB_TOKEN
39
- # remote:
40
- # arch: amd64
41
- # host: ssh://app@192.168.0.1
62
+ # Use a persistent storage volume.
63
+ #
64
+ # volumes:
65
+ # - "app_storage:/app/storage"
42
66
 
43
- # Use accessory services (secrets come from .env).
67
+ # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
68
+ # hitting 404 on in-flight requests. Combines all files from new and old
69
+ # version inside the asset_path.
70
+ #
71
+ # asset_path: /app/public/assets
72
+
73
+ # Configure rolling deploys by setting a wait time between batches of restarts.
74
+ #
75
+ # boot:
76
+ # limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
77
+ # wait: 2
78
+
79
+ # Use accessory services (secrets come from .kamal/secrets).
80
+ #
44
81
  # accessories:
45
82
  # db:
46
83
  # image: mysql:8.0
@@ -57,45 +94,8 @@ registry:
57
94
  # directories:
58
95
  # - data:/var/lib/mysql
59
96
  # redis:
60
- # image: redis:7.0
97
+ # image: valkey/valkey:8
61
98
  # host: 192.168.0.2
62
99
  # port: 6379
63
100
  # directories:
64
101
  # - data:/data
65
-
66
- # Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it.
67
- # traefik:
68
- # args:
69
- # accesslog: true
70
- # accesslog.format: json
71
-
72
- # Configure a custom healthcheck (default is /up on port 3000)
73
- # healthcheck:
74
- # path: /healthz
75
- # port: 4000
76
-
77
- # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
78
- # hitting 404 on in-flight requests. Combines all files from new and old
79
- # version inside the asset_path.
80
- #
81
- # If your app is using the Sprockets gem, ensure it sets `config.assets.manifest`.
82
- # See https://github.com/basecamp/kamal/issues/626 for details
83
- #
84
- # asset_path: /rails/public/assets
85
-
86
- # Configure rolling deploys by setting a wait time between batches of restarts.
87
- # boot:
88
- # limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
89
- # wait: 2
90
-
91
- # Configure the role used to determine the primary_host. This host takes
92
- # deploy locks, runs health checks during the deploy, and follow logs, etc.
93
- #
94
- # Caution: there's no support for role renaming yet, so be careful to cleanup
95
- # the previous role on the deployed hosts.
96
- # primary_role: web
97
-
98
- # Controls if we abort when see a role with no hosts. Disabling this may be
99
- # useful for more complex deploy configurations.
100
- #
101
- # allow_empty_roles: false
@@ -1,13 +1,3 @@
1
- #!/usr/bin/env ruby
1
+ #!/bin/sh
2
2
 
3
- # A sample docker-setup hook
4
- #
5
- # Sets up a Docker network on defined hosts which can then be used by the application’s containers
6
-
7
- hosts = ENV["KAMAL_HOSTS"].split(",")
8
-
9
- hosts.each do |ip|
10
- destination = "root@#{ip}"
11
- puts "Creating a Docker network \"kamal\" on #{destination}"
12
- `ssh #{destination} docker network create kamal`
13
- end
3
+ echo "Docker set up on $KAMAL_HOSTS..."
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
@@ -7,7 +7,7 @@
7
7
  # KAMAL_PERFORMER
8
8
  # KAMAL_VERSION
9
9
  # KAMAL_HOSTS
10
- # KAMAL_ROLE (if set)
10
+ # KAMAL_ROLES (if set)
11
11
  # KAMAL_DESTINATION (if set)
12
12
  # KAMAL_RUNTIME
13
13
 
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
@@ -13,7 +13,7 @@
13
13
  # KAMAL_PERFORMER
14
14
  # KAMAL_VERSION
15
15
  # KAMAL_HOSTS
16
- # KAMAL_ROLE (if set)
16
+ # KAMAL_ROLES (if set)
17
17
  # KAMAL_DESTINATION (if set)
18
18
 
19
19
  if [ -n "$(git status --porcelain)" ]; then
@@ -9,7 +9,7 @@
9
9
  # KAMAL_PERFORMER
10
10
  # KAMAL_VERSION
11
11
  # KAMAL_HOSTS
12
- # KAMAL_ROLE (if set)
12
+ # KAMAL_ROLES (if set)
13
13
  # KAMAL_DESTINATION (if set)
14
14
  # KAMAL_RUNTIME
15
15
 
@@ -13,7 +13,7 @@
13
13
  # KAMAL_HOSTS
14
14
  # KAMAL_COMMAND
15
15
  # KAMAL_SUBCOMMAND
16
- # KAMAL_ROLE (if set)
16
+ # KAMAL_ROLES (if set)
17
17
  # KAMAL_DESTINATION (if set)
18
18
 
19
19
  # Only check the build status for production deployments
@@ -43,7 +43,7 @@ class GithubStatusChecks
43
43
  attr_reader :remote_url, :git_sha, :github_client, :combined_status
44
44
 
45
45
  def initialize
46
- @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
46
+ @remote_url = github_repo_from_remote_url
47
47
  @git_sha = `git rev-parse HEAD`.strip
48
48
  @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
49
49
  refresh!
@@ -77,16 +77,29 @@ class GithubStatusChecks
77
77
  "Build not started..."
78
78
  end
79
79
  end
80
+
81
+ private
82
+ def github_repo_from_remote_url
83
+ url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
84
+ if url.start_with?("https://github.com/")
85
+ url.delete_prefix("https://github.com/")
86
+ elsif url.start_with?("git@github.com:")
87
+ url.delete_prefix("git@github.com:")
88
+ else
89
+ url
90
+ end
91
+ end
80
92
  end
81
93
 
82
94
 
83
95
  $stdout.sync = true
84
96
 
85
- puts "Checking build status..."
86
- attempts = 0
87
- checks = GithubStatusChecks.new
88
-
89
97
  begin
98
+ puts "Checking build status..."
99
+
100
+ attempts = 0
101
+ checks = GithubStatusChecks.new
102
+
90
103
  loop do
91
104
  case checks.state
92
105
  when "success"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
@@ -0,0 +1,17 @@
1
+ # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
2
+ # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
3
+ # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
4
+
5
+ # Option 1: Read secrets from the environment
6
+ KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
7
+
8
+ # Option 2: Read secrets via a command
9
+ # RAILS_MASTER_KEY=$(cat config/master.key)
10
+
11
+ # Option 3: Read secrets via kamal secrets helpers
12
+ # These will handle logging in and fetching the secrets in as few calls as possible
13
+ # There are adapters for 1Password, LastPass + Bitwarden
14
+ #
15
+ # SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
16
+ # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
17
+ # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
data/lib/kamal/cli.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  module Kamal::Cli
2
+ class BootError < StandardError; end
2
3
  class HookError < StandardError; end
3
4
  class LockError < StandardError; end
5
+ class DependencyError < StandardError; end
4
6
  end
5
7
 
6
8
  # SSHKit uses instance eval, so we need a global const for ergonomics