kamal-insecure 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 +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +313 -0
  6. data/lib/kamal/cli/alias/command.rb +10 -0
  7. data/lib/kamal/cli/app/assets.rb +24 -0
  8. data/lib/kamal/cli/app/boot.rb +126 -0
  9. data/lib/kamal/cli/app/error_pages.rb +33 -0
  10. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  11. data/lib/kamal/cli/app.rb +400 -0
  12. data/lib/kamal/cli/base.rb +223 -0
  13. data/lib/kamal/cli/build/clone.rb +61 -0
  14. data/lib/kamal/cli/build.rb +204 -0
  15. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  16. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  17. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  18. data/lib/kamal/cli/lock.rb +45 -0
  19. data/lib/kamal/cli/main.rb +277 -0
  20. data/lib/kamal/cli/proxy.rb +290 -0
  21. data/lib/kamal/cli/prune.rb +34 -0
  22. data/lib/kamal/cli/registry.rb +19 -0
  23. data/lib/kamal/cli/secrets.rb +49 -0
  24. data/lib/kamal/cli/server.rb +50 -0
  25. data/lib/kamal/cli/templates/deploy.yml +101 -0
  26. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  28. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  31. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  35. data/lib/kamal/cli/templates/secrets +17 -0
  36. data/lib/kamal/cli.rb +9 -0
  37. data/lib/kamal/commander/specifics.rb +62 -0
  38. data/lib/kamal/commander.rb +167 -0
  39. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  40. data/lib/kamal/commands/accessory.rb +113 -0
  41. data/lib/kamal/commands/app/assets.rb +51 -0
  42. data/lib/kamal/commands/app/containers.rb +31 -0
  43. data/lib/kamal/commands/app/error_pages.rb +9 -0
  44. data/lib/kamal/commands/app/execution.rb +32 -0
  45. data/lib/kamal/commands/app/images.rb +13 -0
  46. data/lib/kamal/commands/app/logging.rb +28 -0
  47. data/lib/kamal/commands/app/proxy.rb +32 -0
  48. data/lib/kamal/commands/app.rb +124 -0
  49. data/lib/kamal/commands/auditor.rb +39 -0
  50. data/lib/kamal/commands/base.rb +134 -0
  51. data/lib/kamal/commands/builder/base.rb +124 -0
  52. data/lib/kamal/commands/builder/clone.rb +31 -0
  53. data/lib/kamal/commands/builder/cloud.rb +22 -0
  54. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  55. data/lib/kamal/commands/builder/local.rb +14 -0
  56. data/lib/kamal/commands/builder/pack.rb +46 -0
  57. data/lib/kamal/commands/builder/remote.rb +63 -0
  58. data/lib/kamal/commands/builder.rb +48 -0
  59. data/lib/kamal/commands/docker.rb +34 -0
  60. data/lib/kamal/commands/hook.rb +20 -0
  61. data/lib/kamal/commands/lock.rb +70 -0
  62. data/lib/kamal/commands/proxy.rb +127 -0
  63. data/lib/kamal/commands/prune.rb +38 -0
  64. data/lib/kamal/commands/registry.rb +16 -0
  65. data/lib/kamal/commands/server.rb +15 -0
  66. data/lib/kamal/commands.rb +2 -0
  67. data/lib/kamal/configuration/accessory.rb +241 -0
  68. data/lib/kamal/configuration/alias.rb +15 -0
  69. data/lib/kamal/configuration/boot.rb +25 -0
  70. data/lib/kamal/configuration/builder.rb +211 -0
  71. data/lib/kamal/configuration/docs/accessory.yml +128 -0
  72. data/lib/kamal/configuration/docs/alias.yml +26 -0
  73. data/lib/kamal/configuration/docs/boot.yml +19 -0
  74. data/lib/kamal/configuration/docs/builder.yml +132 -0
  75. data/lib/kamal/configuration/docs/configuration.yml +184 -0
  76. data/lib/kamal/configuration/docs/env.yml +116 -0
  77. data/lib/kamal/configuration/docs/logging.yml +21 -0
  78. data/lib/kamal/configuration/docs/proxy.yml +164 -0
  79. data/lib/kamal/configuration/docs/registry.yml +56 -0
  80. data/lib/kamal/configuration/docs/role.yml +53 -0
  81. data/lib/kamal/configuration/docs/servers.yml +27 -0
  82. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  83. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  84. data/lib/kamal/configuration/env/tag.rb +13 -0
  85. data/lib/kamal/configuration/env.rb +38 -0
  86. data/lib/kamal/configuration/logging.rb +33 -0
  87. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  88. data/lib/kamal/configuration/proxy.rb +124 -0
  89. data/lib/kamal/configuration/registry.rb +32 -0
  90. data/lib/kamal/configuration/role.rb +222 -0
  91. data/lib/kamal/configuration/servers.rb +25 -0
  92. data/lib/kamal/configuration/ssh.rb +57 -0
  93. data/lib/kamal/configuration/sshkit.rb +22 -0
  94. data/lib/kamal/configuration/validation.rb +27 -0
  95. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  96. data/lib/kamal/configuration/validator/alias.rb +15 -0
  97. data/lib/kamal/configuration/validator/builder.rb +15 -0
  98. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  99. data/lib/kamal/configuration/validator/env.rb +54 -0
  100. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  101. data/lib/kamal/configuration/validator/registry.rb +25 -0
  102. data/lib/kamal/configuration/validator/role.rb +13 -0
  103. data/lib/kamal/configuration/validator/servers.rb +7 -0
  104. data/lib/kamal/configuration/validator.rb +191 -0
  105. data/lib/kamal/configuration/volume.rb +22 -0
  106. data/lib/kamal/configuration.rb +372 -0
  107. data/lib/kamal/docker.rb +30 -0
  108. data/lib/kamal/env_file.rb +44 -0
  109. data/lib/kamal/git.rb +37 -0
  110. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  111. data/lib/kamal/secrets/adapters/base.rb +33 -0
  112. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  113. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  114. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  115. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  116. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  117. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  118. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  119. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  120. data/lib/kamal/secrets/adapters/test.rb +14 -0
  121. data/lib/kamal/secrets/adapters.rb +16 -0
  122. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  123. data/lib/kamal/secrets.rb +42 -0
  124. data/lib/kamal/sshkit_with_ext.rb +142 -0
  125. data/lib/kamal/tags.rb +40 -0
  126. data/lib/kamal/utils/sensitive.rb +20 -0
  127. data/lib/kamal/utils.rb +110 -0
  128. data/lib/kamal/version.rb +3 -0
  129. data/lib/kamal.rb +14 -0
  130. metadata +365 -0
@@ -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
@@ -0,0 +1,34 @@
1
+ class Kamal::Cli::Prune < Kamal::Cli::Base
2
+ desc "all", "Prune unused images and stopped containers"
3
+ def all
4
+ with_lock do
5
+ containers
6
+ images
7
+ end
8
+ end
9
+
10
+ desc "images", "Prune unused images"
11
+ def images
12
+ with_lock do
13
+ on(KAMAL.hosts) do
14
+ execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
15
+ execute *KAMAL.prune.dangling_images
16
+ execute *KAMAL.prune.tagged_images
17
+ end
18
+ end
19
+ end
20
+
21
+ desc "containers", "Prune all stopped containers, except the last n (default 5)"
22
+ option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
23
+ def containers
24
+ retain = options.fetch(:retain, KAMAL.config.retain_containers)
25
+ raise "retain must be at least 1" if retain < 1
26
+
27
+ with_lock do
28
+ on(KAMAL.hosts) do
29
+ execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
30
+ execute *KAMAL.prune.app_containers(retain: retain)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ class Kamal::Cli::Registry < Kamal::Cli::Base
2
+ desc "login", "Log in to registry locally and remotely"
3
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
4
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
5
+ def login
6
+ ensure_docker_installed unless options[:skip_local]
7
+
8
+ run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
9
+ on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
10
+ end
11
+
12
+ desc "logout", "Log out of registry locally and remotely"
13
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
14
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
15
+ def logout
16
+ run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
17
+ on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
18
+ end
19
+ 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
@@ -0,0 +1,50 @@
1
+ class Kamal::Cli::Server < Kamal::Cli::Base
2
+ desc "exec", "Run a custom command on the server (use --help to show options)"
3
+ option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
4
+ def exec(*cmd)
5
+ pre_connect_if_required
6
+
7
+ cmd = Kamal::Utils.join_commands(cmd)
8
+ hosts = KAMAL.hosts
9
+
10
+ case
11
+ when options[:interactive]
12
+ host = KAMAL.primary_host
13
+
14
+ say "Running '#{cmd}' on #{host} interactively...", :magenta
15
+
16
+ run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
17
+ else
18
+ say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
19
+
20
+ on(hosts) do |host|
21
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
22
+ puts_by_host host, capture_with_info(cmd)
23
+ end
24
+ end
25
+ end
26
+
27
+ desc "bootstrap", "Set up Docker to run Kamal apps"
28
+ def bootstrap
29
+ with_lock do
30
+ missing = []
31
+
32
+ on(KAMAL.hosts) do |host|
33
+ unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
34
+ if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
35
+ info "Missing Docker on #{host}. Installing…"
36
+ execute *KAMAL.docker.install
37
+ else
38
+ missing << host
39
+ end
40
+ end
41
+ end
42
+
43
+ if missing.any?
44
+ raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
45
+ end
46
+
47
+ run_hook "docker-setup"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,101 @@
1
+ # Name of your application. Used to uniquely configure containers.
2
+ service: my-app
3
+
4
+ # Name of the container image.
5
+ image: my-user/my-app
6
+
7
+ # Deploy to these servers.
8
+ servers:
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
25
+
26
+ # Credentials for your image host.
27
+ registry:
28
+ # Specify the registry server, if you're not using Docker Hub
29
+ # server: registry.digitalocean.com / ghcr.io / ...
30
+ username: my-user
31
+
32
+ # Always use an access token rather than real password (pulled from .kamal/secrets).
33
+ password:
34
+ - KAMAL_REGISTRY_PASSWORD
35
+
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
+ #
45
+ # env:
46
+ # clear:
47
+ # DB_HOST: 192.168.0.2
48
+ # secret:
49
+ # - RAILS_MASTER_KEY
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
+
57
+ # Use a different ssh user than root
58
+ #
59
+ # ssh:
60
+ # user: app
61
+
62
+ # Use a persistent storage volume.
63
+ #
64
+ # volumes:
65
+ # - "app_storage:/app/storage"
66
+
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
+ #
81
+ # accessories:
82
+ # db:
83
+ # image: mysql:8.0
84
+ # host: 192.168.0.2
85
+ # port: 3306
86
+ # env:
87
+ # clear:
88
+ # MYSQL_ROOT_HOST: '%'
89
+ # secret:
90
+ # - MYSQL_ROOT_PASSWORD
91
+ # files:
92
+ # - config/mysql/production.cnf:/etc/mysql/my.cnf
93
+ # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
94
+ # directories:
95
+ # - data:/var/lib/mysql
96
+ # redis:
97
+ # image: valkey/valkey:8
98
+ # host: 192.168.0.2
99
+ # port: 6379
100
+ # directories:
101
+ # - data:/data
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
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..."
@@ -0,0 +1,14 @@
1
+ #!/bin/sh
2
+
3
+ # A sample post-deploy hook
4
+ #
5
+ # These environment variables are available:
6
+ # KAMAL_RECORDED_AT
7
+ # KAMAL_PERFORMER
8
+ # KAMAL_VERSION
9
+ # KAMAL_HOSTS
10
+ # KAMAL_ROLES (if set)
11
+ # KAMAL_DESTINATION (if set)
12
+ # KAMAL_RUNTIME
13
+
14
+ echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
@@ -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..."
@@ -0,0 +1,51 @@
1
+ #!/bin/sh
2
+
3
+ # A sample pre-build hook
4
+ #
5
+ # Checks:
6
+ # 1. We have a clean checkout
7
+ # 2. A remote is configured
8
+ # 3. The branch has been pushed to the remote
9
+ # 4. The version we are deploying matches the remote
10
+ #
11
+ # These environment variables are available:
12
+ # KAMAL_RECORDED_AT
13
+ # KAMAL_PERFORMER
14
+ # KAMAL_VERSION
15
+ # KAMAL_HOSTS
16
+ # KAMAL_ROLES (if set)
17
+ # KAMAL_DESTINATION (if set)
18
+
19
+ if [ -n "$(git status --porcelain)" ]; then
20
+ echo "Git checkout is not clean, aborting..." >&2
21
+ git status --porcelain >&2
22
+ exit 1
23
+ fi
24
+
25
+ first_remote=$(git remote)
26
+
27
+ if [ -z "$first_remote" ]; then
28
+ echo "No git remote set, aborting..." >&2
29
+ exit 1
30
+ fi
31
+
32
+ current_branch=$(git branch --show-current)
33
+
34
+ if [ -z "$current_branch" ]; then
35
+ echo "Not on a git branch, aborting..." >&2
36
+ exit 1
37
+ fi
38
+
39
+ remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
40
+
41
+ if [ -z "$remote_head" ]; then
42
+ echo "Branch not pushed to remote, aborting..." >&2
43
+ exit 1
44
+ fi
45
+
46
+ if [ "$KAMAL_VERSION" != "$remote_head" ]; then
47
+ echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
48
+ exit 1
49
+ fi
50
+
51
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A sample pre-connect check
4
+ #
5
+ # Warms DNS before connecting to hosts in parallel
6
+ #
7
+ # These environment variables are available:
8
+ # KAMAL_RECORDED_AT
9
+ # KAMAL_PERFORMER
10
+ # KAMAL_VERSION
11
+ # KAMAL_HOSTS
12
+ # KAMAL_ROLES (if set)
13
+ # KAMAL_DESTINATION (if set)
14
+ # KAMAL_RUNTIME
15
+
16
+ hosts = ENV["KAMAL_HOSTS"].split(",")
17
+ results = nil
18
+ max = 3
19
+
20
+ elapsed = Benchmark.realtime do
21
+ results = hosts.map do |host|
22
+ Thread.new do
23
+ tries = 1
24
+
25
+ begin
26
+ Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
27
+ rescue SocketError
28
+ if tries < max
29
+ puts "Retrying DNS warmup: #{host}"
30
+ tries += 1
31
+ sleep rand
32
+ retry
33
+ else
34
+ puts "DNS warmup failed: #{host}"
35
+ host
36
+ end
37
+ end
38
+
39
+ tries
40
+ end
41
+ end.map(&:value)
42
+ end
43
+
44
+ retries = results.sum - hosts.size
45
+ nopes = results.count { |r| r == max }
46
+
47
+ puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]