nocoffee-kamal 2.3.0.1

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 (114) 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 +287 -0
  6. data/lib/kamal/cli/alias/command.rb +9 -0
  7. data/lib/kamal/cli/app/boot.rb +125 -0
  8. data/lib/kamal/cli/app/prepare_assets.rb +24 -0
  9. data/lib/kamal/cli/app.rb +335 -0
  10. data/lib/kamal/cli/base.rb +198 -0
  11. data/lib/kamal/cli/build/clone.rb +61 -0
  12. data/lib/kamal/cli/build.rb +162 -0
  13. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  14. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  15. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  16. data/lib/kamal/cli/lock.rb +45 -0
  17. data/lib/kamal/cli/main.rb +279 -0
  18. data/lib/kamal/cli/proxy.rb +257 -0
  19. data/lib/kamal/cli/prune.rb +34 -0
  20. data/lib/kamal/cli/registry.rb +17 -0
  21. data/lib/kamal/cli/secrets.rb +43 -0
  22. data/lib/kamal/cli/server.rb +48 -0
  23. data/lib/kamal/cli/templates/deploy.yml +98 -0
  24. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  25. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  26. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  28. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  29. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
  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 +8 -0
  33. data/lib/kamal/commander/specifics.rb +54 -0
  34. data/lib/kamal/commander.rb +176 -0
  35. data/lib/kamal/commands/accessory.rb +113 -0
  36. data/lib/kamal/commands/app/assets.rb +51 -0
  37. data/lib/kamal/commands/app/containers.rb +31 -0
  38. data/lib/kamal/commands/app/execution.rb +30 -0
  39. data/lib/kamal/commands/app/images.rb +13 -0
  40. data/lib/kamal/commands/app/logging.rb +18 -0
  41. data/lib/kamal/commands/app/proxy.rb +16 -0
  42. data/lib/kamal/commands/app.rb +115 -0
  43. data/lib/kamal/commands/auditor.rb +33 -0
  44. data/lib/kamal/commands/base.rb +98 -0
  45. data/lib/kamal/commands/builder/base.rb +111 -0
  46. data/lib/kamal/commands/builder/clone.rb +31 -0
  47. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  48. data/lib/kamal/commands/builder/local.rb +14 -0
  49. data/lib/kamal/commands/builder/remote.rb +63 -0
  50. data/lib/kamal/commands/builder.rb +56 -0
  51. data/lib/kamal/commands/docker.rb +34 -0
  52. data/lib/kamal/commands/hook.rb +20 -0
  53. data/lib/kamal/commands/lock.rb +70 -0
  54. data/lib/kamal/commands/proxy.rb +87 -0
  55. data/lib/kamal/commands/prune.rb +38 -0
  56. data/lib/kamal/commands/registry.rb +14 -0
  57. data/lib/kamal/commands/server.rb +15 -0
  58. data/lib/kamal/commands.rb +2 -0
  59. data/lib/kamal/configuration/accessory.rb +186 -0
  60. data/lib/kamal/configuration/alias.rb +15 -0
  61. data/lib/kamal/configuration/boot.rb +25 -0
  62. data/lib/kamal/configuration/builder.rb +191 -0
  63. data/lib/kamal/configuration/docs/accessory.yml +100 -0
  64. data/lib/kamal/configuration/docs/alias.yml +26 -0
  65. data/lib/kamal/configuration/docs/boot.yml +19 -0
  66. data/lib/kamal/configuration/docs/builder.yml +110 -0
  67. data/lib/kamal/configuration/docs/configuration.yml +178 -0
  68. data/lib/kamal/configuration/docs/env.yml +85 -0
  69. data/lib/kamal/configuration/docs/logging.yml +21 -0
  70. data/lib/kamal/configuration/docs/proxy.yml +110 -0
  71. data/lib/kamal/configuration/docs/registry.yml +52 -0
  72. data/lib/kamal/configuration/docs/role.yml +53 -0
  73. data/lib/kamal/configuration/docs/servers.yml +27 -0
  74. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  75. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  76. data/lib/kamal/configuration/env/tag.rb +13 -0
  77. data/lib/kamal/configuration/env.rb +29 -0
  78. data/lib/kamal/configuration/logging.rb +33 -0
  79. data/lib/kamal/configuration/proxy.rb +63 -0
  80. data/lib/kamal/configuration/registry.rb +32 -0
  81. data/lib/kamal/configuration/role.rb +220 -0
  82. data/lib/kamal/configuration/servers.rb +18 -0
  83. data/lib/kamal/configuration/ssh.rb +57 -0
  84. data/lib/kamal/configuration/sshkit.rb +22 -0
  85. data/lib/kamal/configuration/validation.rb +27 -0
  86. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  87. data/lib/kamal/configuration/validator/alias.rb +15 -0
  88. data/lib/kamal/configuration/validator/builder.rb +13 -0
  89. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  90. data/lib/kamal/configuration/validator/env.rb +54 -0
  91. data/lib/kamal/configuration/validator/proxy.rb +15 -0
  92. data/lib/kamal/configuration/validator/registry.rb +25 -0
  93. data/lib/kamal/configuration/validator/role.rb +11 -0
  94. data/lib/kamal/configuration/validator/servers.rb +7 -0
  95. data/lib/kamal/configuration/validator.rb +171 -0
  96. data/lib/kamal/configuration/volume.rb +22 -0
  97. data/lib/kamal/configuration.rb +393 -0
  98. data/lib/kamal/env_file.rb +44 -0
  99. data/lib/kamal/git.rb +27 -0
  100. data/lib/kamal/secrets/adapters/base.rb +23 -0
  101. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  102. data/lib/kamal/secrets/adapters/last_pass.rb +39 -0
  103. data/lib/kamal/secrets/adapters/one_password.rb +70 -0
  104. data/lib/kamal/secrets/adapters/test.rb +14 -0
  105. data/lib/kamal/secrets/adapters.rb +14 -0
  106. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
  107. data/lib/kamal/secrets.rb +42 -0
  108. data/lib/kamal/sshkit_with_ext.rb +142 -0
  109. data/lib/kamal/tags.rb +40 -0
  110. data/lib/kamal/utils/sensitive.rb +20 -0
  111. data/lib/kamal/utils.rb +110 -0
  112. data/lib/kamal/version.rb +3 -0
  113. data/lib/kamal.rb +14 -0
  114. metadata +349 -0
@@ -0,0 +1,257 @@
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_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_MINIMUM_VERSION}"
18
+ end
19
+ execute *KAMAL.proxy.start_or_run
20
+ end
21
+ end
22
+ end
23
+
24
+ desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
25
+ option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
26
+ option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
27
+ option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
28
+ option :log_max_size, type: :string, default: Kamal::Configuration::PROXY_LOG_MAX_SIZE, desc: "Max size of proxy logs"
29
+ option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
30
+ def boot_config(subcommand)
31
+ case subcommand
32
+ when "set"
33
+ boot_options = [
34
+ *(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
35
+ *(KAMAL.config.proxy_logging_args(options[:log_max_size])),
36
+ *options[:docker_options].map { |option| "--#{option}" }
37
+ ]
38
+
39
+ on(KAMAL.proxy_hosts) do |host|
40
+ execute(*KAMAL.proxy.ensure_proxy_directory)
41
+ upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file
42
+ end
43
+ when "get"
44
+ on(KAMAL.proxy_hosts) do |host|
45
+ puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.get_boot_options)}"
46
+ end
47
+ when "reset"
48
+ on(KAMAL.proxy_hosts) do |host|
49
+ execute *KAMAL.proxy.reset_boot_options
50
+ end
51
+ else
52
+ raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
53
+ end
54
+ end
55
+
56
+ desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
57
+ option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
58
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
59
+ def reboot
60
+ confirming "This will cause a brief outage on each host. Are you sure?" do
61
+ with_lock do
62
+ host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ]
63
+ host_groups.each do |hosts|
64
+ host_list = Array(hosts).join(",")
65
+ run_hook "pre-proxy-reboot", hosts: host_list
66
+ on(hosts) do |host|
67
+ execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
68
+ execute *KAMAL.registry.login
69
+
70
+ "Stopping and removing Traefik on #{host}, if running..."
71
+ execute *KAMAL.proxy.cleanup_traefik
72
+
73
+ "Stopping and removing kamal-proxy on #{host}, if running..."
74
+ execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
75
+ execute *KAMAL.proxy.remove_container
76
+
77
+ execute *KAMAL.proxy.run
78
+
79
+ KAMAL.roles_on(host).select(&:running_proxy?).each do |role|
80
+ app = KAMAL.app(role: role, host: host)
81
+
82
+ version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
83
+ endpoint = capture_with_info(*app.container_id_for_version(version)).strip
84
+
85
+ if endpoint.present?
86
+ info "Deploying #{endpoint} for role `#{role}` on #{host}..."
87
+ execute *app.deploy(target: endpoint)
88
+ end
89
+ end
90
+ end
91
+ run_hook "post-proxy-reboot", hosts: host_list
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)", hide: true
98
+ option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
99
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
100
+ def upgrade
101
+ invoke_options = { "version" => KAMAL.config.latest_tag }.merge(options)
102
+
103
+ confirming "This will cause a brief outage on each host. Are you sure?" do
104
+ host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ]
105
+ host_groups.each do |hosts|
106
+ host_list = Array(hosts).join(",")
107
+ say "Upgrading proxy on #{host_list}...", :magenta
108
+ run_hook "pre-proxy-reboot", hosts: host_list
109
+ on(hosts) do |host|
110
+ execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
111
+ execute *KAMAL.registry.login
112
+
113
+ "Stopping and removing Traefik on #{host}, if running..."
114
+ execute *KAMAL.proxy.cleanup_traefik
115
+
116
+ "Stopping and removing kamal-proxy on #{host}, if running..."
117
+ execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
118
+ execute *KAMAL.proxy.remove_container
119
+ execute *KAMAL.proxy.remove_image
120
+ end
121
+
122
+ KAMAL.with_specific_hosts(hosts) do
123
+ invoke "kamal:cli:proxy:boot", [], invoke_options
124
+ reset_invocation(Kamal::Cli::Proxy)
125
+ invoke "kamal:cli:app:boot", [], invoke_options
126
+ reset_invocation(Kamal::Cli::App)
127
+ invoke "kamal:cli:prune:all", [], invoke_options
128
+ reset_invocation(Kamal::Cli::Prune)
129
+ end
130
+
131
+ run_hook "post-proxy-reboot", hosts: host_list
132
+ say "Upgraded proxy on #{host_list}", :magenta
133
+ end
134
+ end
135
+ end
136
+
137
+ desc "start", "Start existing proxy container on servers"
138
+ def start
139
+ with_lock do
140
+ on(KAMAL.proxy_hosts) do |host|
141
+ execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
142
+ execute *KAMAL.proxy.start
143
+ end
144
+ end
145
+ end
146
+
147
+ desc "stop", "Stop existing proxy container on servers"
148
+ def stop
149
+ with_lock do
150
+ on(KAMAL.proxy_hosts) do |host|
151
+ execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
152
+ execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
153
+ end
154
+ end
155
+ end
156
+
157
+ desc "restart", "Restart existing proxy container on servers"
158
+ def restart
159
+ with_lock do
160
+ stop
161
+ start
162
+ end
163
+ end
164
+
165
+ desc "details", "Show details about proxy container from servers"
166
+ def details
167
+ on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy" }
168
+ end
169
+
170
+ desc "logs", "Show log lines from proxy on servers"
171
+ option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
172
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
173
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
174
+ option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
175
+ option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
176
+ def logs
177
+ grep = options[:grep]
178
+ timestamps = !options[:skip_timestamps]
179
+
180
+ if options[:follow]
181
+ run_locally do
182
+ info "Following logs on #{KAMAL.primary_host}..."
183
+ info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
184
+ exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
185
+ end
186
+ else
187
+ since = options[:since]
188
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
189
+
190
+ on(KAMAL.proxy_hosts) do |host|
191
+ puts_by_host host, capture(*KAMAL.proxy.logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
192
+ end
193
+ end
194
+ end
195
+
196
+ desc "remove", "Remove proxy container and image from servers"
197
+ option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
198
+ def remove
199
+ with_lock do
200
+ if removal_allowed?(options[:force])
201
+ stop
202
+ remove_container
203
+ remove_image
204
+ remove_proxy_directory
205
+ end
206
+ end
207
+ end
208
+
209
+ desc "remove_container", "Remove proxy container from servers", hide: true
210
+ def remove_container
211
+ with_lock do
212
+ on(KAMAL.proxy_hosts) do
213
+ execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
214
+ execute *KAMAL.proxy.remove_container
215
+ end
216
+ end
217
+ end
218
+
219
+ desc "remove_image", "Remove proxy image from servers", hide: true
220
+ def remove_image
221
+ with_lock do
222
+ on(KAMAL.proxy_hosts) do
223
+ execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
224
+ execute *KAMAL.proxy.remove_image
225
+ end
226
+ end
227
+ end
228
+
229
+ desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
230
+ def remove_proxy_directory
231
+ with_lock do
232
+ on(KAMAL.proxy_hosts) do
233
+ execute *KAMAL.proxy.remove_proxy_directory, raise_on_non_zero_exit: false
234
+ end
235
+ end
236
+ end
237
+
238
+ private
239
+ def removal_allowed?(force)
240
+ on(KAMAL.proxy_hosts) do |host|
241
+ app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i
242
+ raise "The are other applications installed on #{host}" if app_count > 0
243
+ end
244
+
245
+ true
246
+ rescue SSHKit::Runner::ExecuteError => e
247
+ raise unless e.message.include?("The are other applications installed on")
248
+
249
+ if force
250
+ say "Forcing, so removing the proxy, even though other apps are installed", :magenta
251
+ else
252
+ say "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", :magenta
253
+ end
254
+
255
+ force
256
+ end
257
+ 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,17 @@
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
+ run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
7
+ on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
8
+ end
9
+
10
+ desc "logout", "Log out of registry locally and remotely"
11
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
12
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
13
+ def logout
14
+ run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
15
+ on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
16
+ end
17
+ end
@@ -0,0 +1,43 @@
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: true, 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
+ results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
9
+
10
+ return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
11
+ end
12
+
13
+ desc "extract", "Extract a single secret from the results of a fetch call"
14
+ option :inline, type: :boolean, required: false, hidden: true
15
+ def extract(name, secrets)
16
+ parsed_secrets = JSON.parse(secrets)
17
+ value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
18
+
19
+ raise "Could not find secret #{name}" if value.nil?
20
+
21
+ return_or_puts value, inline: options[:inline]
22
+ end
23
+
24
+ desc "print", "Print the secrets (for debugging)"
25
+ def print
26
+ KAMAL.config.secrets.to_h.each do |key, value|
27
+ puts "#{key}=#{value}"
28
+ end
29
+ end
30
+
31
+ private
32
+ def adapter(adapter)
33
+ Kamal::Secrets::Adapters.lookup(adapter)
34
+ end
35
+
36
+ def return_or_puts(value, inline: nil)
37
+ if inline
38
+ value
39
+ else
40
+ puts value
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
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
+ cmd = Kamal::Utils.join_commands(cmd)
6
+ hosts = KAMAL.hosts | KAMAL.accessory_hosts
7
+
8
+ case
9
+ when options[:interactive]
10
+ host = KAMAL.primary_host
11
+
12
+ say "Running '#{cmd}' on #{host} interactively...", :magenta
13
+
14
+ run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
15
+ else
16
+ say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
17
+
18
+ on(hosts) do |host|
19
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
20
+ puts_by_host host, capture_with_info(cmd)
21
+ end
22
+ end
23
+ end
24
+
25
+ desc "bootstrap", "Set up Docker to run Kamal apps"
26
+ def bootstrap
27
+ with_lock do
28
+ missing = []
29
+
30
+ on(KAMAL.hosts | KAMAL.accessory_hosts) do |host|
31
+ unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
32
+ if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
33
+ info "Missing Docker on #{host}. Installing…"
34
+ execute *KAMAL.docker.install
35
+ else
36
+ missing << host
37
+ end
38
+ end
39
+ end
40
+
41
+ if missing.any?
42
+ 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/"
43
+ end
44
+
45
+ run_hook "docker-setup"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,98 @@
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
+
40
+ # Inject ENV variables into containers (secrets come from .kamal/secrets).
41
+ #
42
+ # env:
43
+ # clear:
44
+ # DB_HOST: 192.168.0.2
45
+ # secret:
46
+ # - RAILS_MASTER_KEY
47
+
48
+ # Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
49
+ # "bin/kamal logs -r job" will tail logs from the first server in the job section.
50
+ #
51
+ # aliases:
52
+ # shell: app exec --interactive --reuse "bash"
53
+
54
+ # Use a different ssh user than root
55
+ #
56
+ # ssh:
57
+ # user: app
58
+
59
+ # Use a persistent storage volume.
60
+ #
61
+ # volumes:
62
+ # - "app_storage:/app/storage"
63
+
64
+ # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
65
+ # hitting 404 on in-flight requests. Combines all files from new and old
66
+ # version inside the asset_path.
67
+ #
68
+ # asset_path: /app/public/assets
69
+
70
+ # Configure rolling deploys by setting a wait time between batches of restarts.
71
+ #
72
+ # boot:
73
+ # limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
74
+ # wait: 2
75
+
76
+ # Use accessory services (secrets come from .kamal/secrets).
77
+ #
78
+ # accessories:
79
+ # db:
80
+ # image: mysql:8.0
81
+ # host: 192.168.0.2
82
+ # port: 3306
83
+ # env:
84
+ # clear:
85
+ # MYSQL_ROOT_HOST: '%'
86
+ # secret:
87
+ # - MYSQL_ROOT_PASSWORD
88
+ # files:
89
+ # - config/mysql/production.cnf:/etc/mysql/my.cnf
90
+ # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
91
+ # directories:
92
+ # - data:/var/lib/mysql
93
+ # redis:
94
+ # image: valkey/valkey:8
95
+ # host: 192.168.0.2
96
+ # port: 6379
97
+ # directories:
98
+ # - data:/data
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Docker set up 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_ROLE (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,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_ROLE (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_ROLE (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 ]