kamal 0.16.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/kamal/cli/app.rb +44 -13
- data/lib/kamal/cli/base.rb +15 -2
- data/lib/kamal/cli/build.rb +18 -1
- data/lib/kamal/cli/env.rb +56 -0
- data/lib/kamal/cli/healthcheck/poller.rb +64 -0
- data/lib/kamal/cli/healthcheck.rb +2 -2
- data/lib/kamal/cli/lock.rb +12 -3
- data/lib/kamal/cli/main.rb +18 -4
- data/lib/kamal/cli/prune.rb +3 -2
- data/lib/kamal/cli/server.rb +2 -0
- data/lib/kamal/cli/templates/deploy.yml +12 -1
- data/lib/kamal/commander.rb +21 -8
- data/lib/kamal/commands/accessory.rb +8 -8
- data/lib/kamal/commands/app/assets.rb +51 -0
- data/lib/kamal/commands/app/containers.rb +23 -0
- data/lib/kamal/commands/app/cord.rb +22 -0
- data/lib/kamal/commands/app/execution.rb +27 -0
- data/lib/kamal/commands/app/images.rb +13 -0
- data/lib/kamal/commands/app/logging.rb +18 -0
- data/lib/kamal/commands/app.rb +18 -91
- data/lib/kamal/commands/auditor.rb +3 -1
- data/lib/kamal/commands/base.rb +12 -0
- data/lib/kamal/commands/builder/base.rb +6 -0
- data/lib/kamal/commands/builder.rb +1 -1
- data/lib/kamal/commands/docker.rb +1 -1
- data/lib/kamal/commands/healthcheck.rb +15 -12
- data/lib/kamal/commands/lock.rb +2 -2
- data/lib/kamal/commands/prune.rb +11 -3
- data/lib/kamal/commands/server.rb +5 -0
- data/lib/kamal/commands/traefik.rb +21 -7
- data/lib/kamal/configuration/accessory.rb +14 -2
- data/lib/kamal/configuration/role.rb +112 -19
- data/lib/kamal/configuration/ssh.rb +1 -1
- data/lib/kamal/configuration/volume.rb +22 -0
- data/lib/kamal/configuration.rb +73 -44
- data/lib/kamal/env_file.rb +41 -0
- data/lib/kamal/git.rb +19 -0
- data/lib/kamal/utils/sensitive.rb +1 -0
- data/lib/kamal/utils.rb +0 -39
- data/lib/kamal/version.rb +1 -1
- metadata +15 -4
- data/lib/kamal/utils/healthcheck_poller.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2470f58e93ad7f181c9a422a0eef08c741190cb897c962b83aeda9effd4862f5
|
4
|
+
data.tar.gz: df8ddd4bf5dcf6687fab747bcfb11689dbe58b846ab6e392e99694ce8bfdff88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cef4c1ea22ffe3a0f80dbd2985cb3f3941e70dfcb35509a4ec7210124e517b0b7336ddd571370c32868d9d99442b9cc1186eb6a0cdefaa5db1b0475f117faae
|
7
|
+
data.tar.gz: 1f6d43624775bb9f1320692c3b4e5d061528b92763886f963cf184978e2405e478c5c6a55347316c69416f512bb5f70c970999d56e2e5c6937b6b83613338106
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Kamal: Deploy web apps anywhere
|
2
2
|
|
3
|
-
From bare metal to cloud VMs
|
3
|
+
From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal has the dynamic reverse-proxy Traefik hold requests while a new app container is started and the old one is stopped. Works seamlessly across multiple hosts, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.
|
4
4
|
|
5
5
|
➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands).
|
6
6
|
|
data/lib/kamal/cli/app.rb
CHANGED
@@ -9,31 +9,56 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
9
9
|
|
10
10
|
on(KAMAL.hosts) do
|
11
11
|
execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
|
12
|
-
execute *KAMAL.app.
|
12
|
+
execute *KAMAL.app.tag_current_image_as_latest
|
13
|
+
|
14
|
+
KAMAL.roles_on(host).each do |role|
|
15
|
+
app = KAMAL.app(role: role)
|
16
|
+
role_config = KAMAL.config.role(role)
|
17
|
+
|
18
|
+
if role_config.assets?
|
19
|
+
execute *app.extract_assets
|
20
|
+
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
21
|
+
execute *app.sync_asset_volumes(old_version: old_version)
|
22
|
+
end
|
23
|
+
end
|
13
24
|
end
|
14
25
|
|
15
26
|
on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
|
16
|
-
|
17
|
-
|
18
|
-
roles.each do |role|
|
27
|
+
KAMAL.roles_on(host).each do |role|
|
19
28
|
app = KAMAL.app(role: role)
|
20
29
|
auditor = KAMAL.auditor(role: role)
|
30
|
+
role_config = KAMAL.config.role(role)
|
21
31
|
|
22
|
-
if capture_with_info(*app.container_id_for_version(version
|
32
|
+
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
|
23
33
|
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
24
34
|
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
25
35
|
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
26
36
|
execute *app.rename_container(version: version, new_version: tmp_version)
|
27
37
|
end
|
28
38
|
|
39
|
+
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
40
|
+
|
41
|
+
execute *app.tie_cord(role_config.cord_host_file) if role_config.uses_cord?
|
42
|
+
|
29
43
|
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
30
44
|
|
31
|
-
|
32
|
-
|
45
|
+
execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
|
46
|
+
|
47
|
+
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
48
|
+
|
49
|
+
if old_version.present?
|
50
|
+
if role_config.uses_cord?
|
51
|
+
cord = capture_with_info(*app.cord(version: old_version), raise_on_non_zero_exit: false).strip
|
52
|
+
if cord.present?
|
53
|
+
execute *app.cut_cord(cord)
|
54
|
+
Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: old_version)) }
|
55
|
+
end
|
56
|
+
end
|
33
57
|
|
34
|
-
|
58
|
+
execute *app.stop(version: old_version), raise_on_non_zero_exit: false
|
35
59
|
|
36
|
-
|
60
|
+
execute *app.clean_up_assets if role_config.assets?
|
61
|
+
end
|
37
62
|
end
|
38
63
|
end
|
39
64
|
end
|
@@ -90,14 +115,16 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
90
115
|
say "Get current version of running container...", :magenta unless options[:version]
|
91
116
|
using_version(options[:version] || current_running_version) do |version|
|
92
117
|
say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
|
93
|
-
run_locally { exec KAMAL.app(role:
|
118
|
+
run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
|
94
119
|
end
|
95
120
|
|
96
121
|
when options[:interactive]
|
97
122
|
say "Get most recent version available as an image...", :magenta unless options[:version]
|
98
123
|
using_version(version_or_latest) do |version|
|
99
124
|
say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
|
100
|
-
run_locally
|
125
|
+
run_locally do
|
126
|
+
exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host)
|
127
|
+
end
|
101
128
|
end
|
102
129
|
|
103
130
|
when options[:reuse]
|
@@ -120,8 +147,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
120
147
|
using_version(version_or_latest) do |version|
|
121
148
|
say "Launching command with version #{version} from new container...", :magenta
|
122
149
|
on(KAMAL.hosts) do |host|
|
123
|
-
|
124
|
-
|
150
|
+
roles = KAMAL.roles_on(host)
|
151
|
+
|
152
|
+
roles.each do |role|
|
153
|
+
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
154
|
+
puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd))
|
155
|
+
end
|
125
156
|
end
|
126
157
|
end
|
127
158
|
end
|
data/lib/kamal/cli/base.rb
CHANGED
@@ -24,6 +24,7 @@ module Kamal::Cli
|
|
24
24
|
|
25
25
|
def initialize(*)
|
26
26
|
super
|
27
|
+
@original_env = ENV.to_h.dup
|
27
28
|
load_envs
|
28
29
|
initialize_commander(options_with_subcommand_class_options)
|
29
30
|
end
|
@@ -37,6 +38,12 @@ module Kamal::Cli
|
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
41
|
+
def reload_envs
|
42
|
+
ENV.clear
|
43
|
+
ENV.update(@original_env)
|
44
|
+
load_envs
|
45
|
+
end
|
46
|
+
|
40
47
|
def options_with_subcommand_class_options
|
41
48
|
options.merge(@_initializer.last[:class_options] || {})
|
42
49
|
end
|
@@ -75,10 +82,10 @@ module Kamal::Cli
|
|
75
82
|
def mutating
|
76
83
|
return yield if KAMAL.holding_lock?
|
77
84
|
|
78
|
-
KAMAL.config.ensure_env_available
|
79
|
-
|
80
85
|
run_hook "pre-connect"
|
81
86
|
|
87
|
+
ensure_run_directory
|
88
|
+
|
82
89
|
acquire_lock
|
83
90
|
|
84
91
|
begin
|
@@ -167,5 +174,11 @@ module Kamal::Cli
|
|
167
174
|
def first_invocation
|
168
175
|
instance_variable_get("@_invocations").first
|
169
176
|
end
|
177
|
+
|
178
|
+
def ensure_run_directory
|
179
|
+
on(KAMAL.hosts) do
|
180
|
+
execute(*KAMAL.server.ensure_run_directory)
|
181
|
+
end
|
182
|
+
end
|
170
183
|
end
|
171
184
|
end
|
data/lib/kamal/cli/build.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
1
3
|
class Kamal::Cli::Build < Kamal::Cli::Base
|
2
4
|
class BuildError < StandardError; end
|
3
5
|
|
@@ -17,7 +19,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|
17
19
|
verify_local_dependencies
|
18
20
|
run_hook "pre-build"
|
19
21
|
|
20
|
-
if (uncommitted_changes = Kamal::
|
22
|
+
if (uncommitted_changes = Kamal::Git.uncommitted_changes).present?
|
21
23
|
say "The following paths have uncommitted changes:\n #{uncommitted_changes}", :yellow
|
22
24
|
end
|
23
25
|
|
@@ -48,6 +50,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|
48
50
|
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
|
49
51
|
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
|
50
52
|
execute *KAMAL.builder.pull
|
53
|
+
execute *KAMAL.builder.validate_image
|
51
54
|
end
|
52
55
|
end
|
53
56
|
end
|
@@ -55,6 +58,10 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|
55
58
|
desc "create", "Create a build setup"
|
56
59
|
def create
|
57
60
|
mutating do
|
61
|
+
if (remote_host = KAMAL.config.builder.remote_host)
|
62
|
+
connect_to_remote_host(remote_host)
|
63
|
+
end
|
64
|
+
|
58
65
|
run_locally do
|
59
66
|
begin
|
60
67
|
debug "Using builder: #{KAMAL.builder.name}"
|
@@ -103,4 +110,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|
103
110
|
end
|
104
111
|
end
|
105
112
|
end
|
113
|
+
|
114
|
+
def connect_to_remote_host(remote_host)
|
115
|
+
remote_uri = URI.parse(remote_host)
|
116
|
+
if remote_uri.scheme == "ssh"
|
117
|
+
options = { user: remote_uri.user, port: remote_uri.port }.compact
|
118
|
+
on(remote_uri.host, options) do
|
119
|
+
execute "true"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
106
123
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
|
3
|
+
class Kamal::Cli::Env < Kamal::Cli::Base
|
4
|
+
desc "push", "Push the env file to the remote hosts"
|
5
|
+
def push
|
6
|
+
mutating do
|
7
|
+
on(KAMAL.hosts) do
|
8
|
+
execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
|
9
|
+
|
10
|
+
KAMAL.roles_on(host).each do |role|
|
11
|
+
role_config = KAMAL.config.role(role)
|
12
|
+
execute *KAMAL.app(role: role).make_env_directory
|
13
|
+
upload! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
on(KAMAL.traefik_hosts) do
|
18
|
+
execute *KAMAL.traefik.make_env_directory
|
19
|
+
upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400
|
20
|
+
end
|
21
|
+
|
22
|
+
on(KAMAL.accessory_hosts) do
|
23
|
+
KAMAL.accessories_on(host).each do |accessory|
|
24
|
+
accessory_config = KAMAL.config.accessory(accessory)
|
25
|
+
execute *KAMAL.accessory(accessory).make_env_directory
|
26
|
+
upload! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "delete", "Delete the env file from the remote hosts"
|
33
|
+
def delete
|
34
|
+
mutating do
|
35
|
+
on(KAMAL.hosts) do
|
36
|
+
execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
|
37
|
+
|
38
|
+
KAMAL.roles_on(host).each do |role|
|
39
|
+
role_config = KAMAL.config.role(role)
|
40
|
+
execute *KAMAL.app(role: role).remove_env_file
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
on(KAMAL.traefik_hosts) do
|
45
|
+
execute *KAMAL.traefik.remove_env_file
|
46
|
+
end
|
47
|
+
|
48
|
+
on(KAMAL.accessory_hosts) do
|
49
|
+
KAMAL.accessories_on(host).each do |accessory|
|
50
|
+
accessory_config = KAMAL.config.accessory(accessory)
|
51
|
+
execute *KAMAL.accessory(accessory).remove_env_file
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Kamal::Cli::Healthcheck::Poller
|
2
|
+
extend self
|
3
|
+
|
4
|
+
TRAEFIK_UPDATE_DELAY = 5
|
5
|
+
|
6
|
+
class HealthcheckError < StandardError; end
|
7
|
+
|
8
|
+
def wait_for_healthy(pause_after_ready: false, &block)
|
9
|
+
attempt = 1
|
10
|
+
max_attempts = KAMAL.config.healthcheck["max_attempts"]
|
11
|
+
|
12
|
+
begin
|
13
|
+
case status = block.call
|
14
|
+
when "healthy"
|
15
|
+
sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
|
16
|
+
when "running" # No health check configured
|
17
|
+
sleep KAMAL.config.readiness_delay if pause_after_ready
|
18
|
+
else
|
19
|
+
raise HealthcheckError, "container not ready (#{status})"
|
20
|
+
end
|
21
|
+
rescue HealthcheckError => e
|
22
|
+
if attempt <= max_attempts
|
23
|
+
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
24
|
+
sleep attempt
|
25
|
+
attempt += 1
|
26
|
+
retry
|
27
|
+
else
|
28
|
+
raise
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
info "Container is healthy!"
|
33
|
+
end
|
34
|
+
|
35
|
+
def wait_for_unhealthy(pause_after_ready: false, &block)
|
36
|
+
attempt = 1
|
37
|
+
max_attempts = KAMAL.config.healthcheck["max_attempts"]
|
38
|
+
|
39
|
+
begin
|
40
|
+
case status = block.call
|
41
|
+
when "unhealthy"
|
42
|
+
sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
|
43
|
+
else
|
44
|
+
raise HealthcheckError, "container not unhealthy (#{status})"
|
45
|
+
end
|
46
|
+
rescue HealthcheckError => e
|
47
|
+
if attempt <= max_attempts
|
48
|
+
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
49
|
+
sleep attempt
|
50
|
+
attempt += 1
|
51
|
+
retry
|
52
|
+
else
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
info "Container is unhealthy!"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def info(message)
|
62
|
+
SSHKit.config.output.info(message)
|
63
|
+
end
|
64
|
+
end
|
@@ -6,8 +6,8 @@ class Kamal::Cli::Healthcheck < Kamal::Cli::Base
|
|
6
6
|
on(KAMAL.primary_host) do
|
7
7
|
begin
|
8
8
|
execute *KAMAL.healthcheck.run
|
9
|
-
|
10
|
-
rescue
|
9
|
+
Poller.wait_for_healthy { capture_with_info(*KAMAL.healthcheck.status) }
|
10
|
+
rescue Poller::HealthcheckError => e
|
11
11
|
error capture_with_info(*KAMAL.healthcheck.logs)
|
12
12
|
error capture_with_pretty_json(*KAMAL.healthcheck.container_health_log)
|
13
13
|
raise
|
data/lib/kamal/cli/lock.rb
CHANGED
@@ -2,7 +2,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|
2
2
|
desc "status", "Report lock status"
|
3
3
|
def status
|
4
4
|
handle_missing_lock do
|
5
|
-
on(KAMAL.primary_host)
|
5
|
+
on(KAMAL.primary_host) do
|
6
|
+
execute *KAMAL.server.ensure_run_directory
|
7
|
+
puts capture_with_debug(*KAMAL.lock.status)
|
8
|
+
end
|
6
9
|
end
|
7
10
|
end
|
8
11
|
|
@@ -11,7 +14,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|
11
14
|
def acquire
|
12
15
|
message = options[:message]
|
13
16
|
raise_if_locked do
|
14
|
-
on(KAMAL.primary_host)
|
17
|
+
on(KAMAL.primary_host) do
|
18
|
+
execute *KAMAL.server.ensure_run_directory
|
19
|
+
execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
|
20
|
+
end
|
15
21
|
say "Acquired the deploy lock"
|
16
22
|
end
|
17
23
|
end
|
@@ -19,7 +25,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|
19
25
|
desc "release", "Release the deploy lock"
|
20
26
|
def release
|
21
27
|
handle_missing_lock do
|
22
|
-
on(KAMAL.primary_host)
|
28
|
+
on(KAMAL.primary_host) do
|
29
|
+
execute *KAMAL.server.ensure_run_directory
|
30
|
+
execute *KAMAL.lock.release, verbosity: :debug
|
31
|
+
end
|
23
32
|
say "Released the deploy lock"
|
24
33
|
end
|
25
34
|
end
|
data/lib/kamal/cli/main.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
class Kamal::Cli::Main < Kamal::Cli::Base
|
2
|
-
desc "setup", "Setup all accessories and deploy app to servers"
|
2
|
+
desc "setup", "Setup all accessories, push the env, and deploy app to servers"
|
3
3
|
def setup
|
4
4
|
print_runtime do
|
5
5
|
mutating do
|
6
|
+
say "Ensure Docker is installed...", :magenta
|
6
7
|
invoke "kamal:cli:server:bootstrap"
|
8
|
+
|
9
|
+
say "Push env files...", :magenta
|
10
|
+
invoke "kamal:cli:env:push"
|
11
|
+
|
7
12
|
invoke "kamal:cli:accessory:boot", [ "all" ]
|
8
13
|
deploy
|
9
14
|
end
|
@@ -37,7 +42,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|
37
42
|
invoke "kamal:cli:healthcheck:perform", [], invoke_options
|
38
43
|
|
39
44
|
say "Detect stale containers...", :magenta
|
40
|
-
invoke "kamal:cli:app:stale_containers", [], invoke_options
|
45
|
+
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
41
46
|
|
42
47
|
invoke "kamal:cli:app:boot", [], invoke_options
|
43
48
|
|
@@ -70,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|
70
75
|
invoke "kamal:cli:healthcheck:perform", [], invoke_options
|
71
76
|
|
72
77
|
say "Detect stale containers...", :magenta
|
73
|
-
invoke "kamal:cli:app:stale_containers", [], invoke_options
|
78
|
+
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
74
79
|
|
75
80
|
invoke "kamal:cli:app:boot", [], invoke_options
|
76
81
|
end
|
@@ -165,6 +170,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|
165
170
|
end
|
166
171
|
|
167
172
|
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
|
173
|
+
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip .env file push"
|
168
174
|
def envify
|
169
175
|
if destination = options[:destination]
|
170
176
|
env_template_path = ".env.#{destination}.erb"
|
@@ -174,7 +180,12 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|
174
180
|
env_path = ".env"
|
175
181
|
end
|
176
182
|
|
177
|
-
File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
|
183
|
+
File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
|
184
|
+
|
185
|
+
unless options[:skip_push]
|
186
|
+
reload_envs
|
187
|
+
invoke "kamal:cli:env:push", options
|
188
|
+
end
|
178
189
|
end
|
179
190
|
|
180
191
|
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
@@ -204,6 +215,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|
204
215
|
desc "build", "Build application image"
|
205
216
|
subcommand "build", Kamal::Cli::Build
|
206
217
|
|
218
|
+
desc "env", "Manage environment files"
|
219
|
+
subcommand "env", Kamal::Cli::Env
|
220
|
+
|
207
221
|
desc "healthcheck", "Healthcheck application"
|
208
222
|
subcommand "healthcheck", Kamal::Cli::Healthcheck
|
209
223
|
|
data/lib/kamal/cli/prune.rb
CHANGED
@@ -7,7 +7,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
-
desc "images", "Prune
|
10
|
+
desc "images", "Prune unused images"
|
11
11
|
def images
|
12
12
|
mutating do
|
13
13
|
on(KAMAL.hosts) do
|
@@ -23,7 +23,8 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
|
|
23
23
|
mutating do
|
24
24
|
on(KAMAL.hosts) do
|
25
25
|
execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
|
26
|
-
execute *KAMAL.prune.
|
26
|
+
execute *KAMAL.prune.app_containers
|
27
|
+
execute *KAMAL.prune.healthcheck_containers
|
27
28
|
end
|
28
29
|
end
|
29
30
|
end
|
data/lib/kamal/cli/server.rb
CHANGED
@@ -19,6 +19,7 @@ registry:
|
|
19
19
|
- KAMAL_REGISTRY_PASSWORD
|
20
20
|
|
21
21
|
# Inject ENV variables into containers (secrets come from .env).
|
22
|
+
# Remember to run `kamal env push` after making changes!
|
22
23
|
# env:
|
23
24
|
# clear:
|
24
25
|
# DB_HOST: 192.168.0.2
|
@@ -52,7 +53,7 @@ registry:
|
|
52
53
|
# - MYSQL_ROOT_PASSWORD
|
53
54
|
# files:
|
54
55
|
# - config/mysql/production.cnf:/etc/mysql/my.cnf
|
55
|
-
# - db/production.sql
|
56
|
+
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
|
56
57
|
# directories:
|
57
58
|
# - data:/var/lib/mysql
|
58
59
|
# redis:
|
@@ -72,3 +73,13 @@ registry:
|
|
72
73
|
# healthcheck:
|
73
74
|
# path: /healthz
|
74
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
|
+
# asset_path: /rails/public/assets
|
81
|
+
|
82
|
+
# Configure rolling deploys by setting a wait time between batches of restarts.
|
83
|
+
# boot:
|
84
|
+
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
|
85
|
+
# wait: 2
|
data/lib/kamal/commander.rb
CHANGED
@@ -39,6 +39,10 @@ class Kamal::Commander
|
|
39
39
|
specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
|
40
40
|
end
|
41
41
|
|
42
|
+
def primary_role
|
43
|
+
roles_on(primary_host).first
|
44
|
+
end
|
45
|
+
|
42
46
|
def roles
|
43
47
|
(specific_roles || config.roles).select do |role|
|
44
48
|
((specific_hosts || config.all_hosts) & role.hosts).any?
|
@@ -51,14 +55,6 @@ class Kamal::Commander
|
|
51
55
|
end
|
52
56
|
end
|
53
57
|
|
54
|
-
def boot_strategy
|
55
|
-
if config.boot.limit.present?
|
56
|
-
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
57
|
-
else
|
58
|
-
{}
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
58
|
def roles_on(host)
|
63
59
|
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
|
64
60
|
end
|
@@ -75,6 +71,10 @@ class Kamal::Commander
|
|
75
71
|
config.accessories&.collect(&:name) || []
|
76
72
|
end
|
77
73
|
|
74
|
+
def accessories_on(host)
|
75
|
+
config.accessories.select { |accessory| accessory.hosts.include?(host.to_s) }.map(&:name)
|
76
|
+
end
|
77
|
+
|
78
78
|
|
79
79
|
def app(role: nil)
|
80
80
|
Kamal::Commands::App.new(config, role: role)
|
@@ -116,10 +116,15 @@ class Kamal::Commander
|
|
116
116
|
@registry ||= Kamal::Commands::Registry.new(config)
|
117
117
|
end
|
118
118
|
|
119
|
+
def server
|
120
|
+
@server ||= Kamal::Commands::Server.new(config)
|
121
|
+
end
|
122
|
+
|
119
123
|
def traefik
|
120
124
|
@traefik ||= Kamal::Commands::Traefik.new(config)
|
121
125
|
end
|
122
126
|
|
127
|
+
|
123
128
|
def with_verbosity(level)
|
124
129
|
old_level = self.verbosity
|
125
130
|
|
@@ -132,6 +137,14 @@ class Kamal::Commander
|
|
132
137
|
SSHKit.config.output_verbosity = old_level
|
133
138
|
end
|
134
139
|
|
140
|
+
def boot_strategy
|
141
|
+
if config.boot.limit.present?
|
142
|
+
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
143
|
+
else
|
144
|
+
{}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
135
148
|
def holding_lock?
|
136
149
|
self.holding_lock
|
137
150
|
end
|
@@ -86,14 +86,6 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
def make_directory_for(remote_file)
|
90
|
-
make_directory Pathname.new(remote_file).dirname.to_s
|
91
|
-
end
|
92
|
-
|
93
|
-
def make_directory(path)
|
94
|
-
[ :mkdir, "-p", path ]
|
95
|
-
end
|
96
|
-
|
97
89
|
def remove_service_directory
|
98
90
|
[ :rm, "-rf", service_name ]
|
99
91
|
end
|
@@ -106,6 +98,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|
106
98
|
docker :image, :rm, "--force", image
|
107
99
|
end
|
108
100
|
|
101
|
+
def make_env_directory
|
102
|
+
make_directory accessory_config.host_env_directory
|
103
|
+
end
|
104
|
+
|
105
|
+
def remove_env_file
|
106
|
+
[:rm, "-f", accessory_config.host_env_file_path]
|
107
|
+
end
|
108
|
+
|
109
109
|
private
|
110
110
|
def service_filter
|
111
111
|
[ "--filter", "label=service=#{service_name}" ]
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Kamal::Commands::App::Assets
|
2
|
+
def extract_assets
|
3
|
+
asset_container = "#{role_config.container_prefix}-assets"
|
4
|
+
|
5
|
+
combine \
|
6
|
+
make_directory(role_config.asset_extracted_path),
|
7
|
+
[*docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true"],
|
8
|
+
docker(:run, "--name", asset_container, "--detach", "--rm", config.latest_image, "sleep 1000000"),
|
9
|
+
docker(:cp, "-L", "#{asset_container}:#{role_config.asset_path}/.", role_config.asset_extracted_path),
|
10
|
+
docker(:stop, "-t 1", asset_container),
|
11
|
+
by: "&&"
|
12
|
+
end
|
13
|
+
|
14
|
+
def sync_asset_volumes(old_version: nil)
|
15
|
+
new_extracted_path, new_volume_path = role_config.asset_extracted_path(config.version), role_config.asset_volume.host_path
|
16
|
+
if old_version.present?
|
17
|
+
old_extracted_path, old_volume_path = role_config.asset_extracted_path(old_version), role_config.asset_volume(old_version).host_path
|
18
|
+
end
|
19
|
+
|
20
|
+
commands = [make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path)]
|
21
|
+
|
22
|
+
if old_version.present?
|
23
|
+
commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
|
24
|
+
commands << copy_contents(old_extracted_path, new_volume_path, continue_on_error: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
chain *commands
|
28
|
+
end
|
29
|
+
|
30
|
+
def clean_up_assets
|
31
|
+
chain \
|
32
|
+
find_and_remove_older_siblings(role_config.asset_extracted_path),
|
33
|
+
find_and_remove_older_siblings(role_config.asset_volume_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def find_and_remove_older_siblings(path)
|
38
|
+
[
|
39
|
+
:find,
|
40
|
+
Pathname.new(path).dirname.to_s,
|
41
|
+
"-maxdepth 1",
|
42
|
+
"-name", "'#{role_config.container_prefix}-*'",
|
43
|
+
"!", "-name", Pathname.new(path).basename.to_s,
|
44
|
+
"-exec rm -rf \"{}\" +"
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def copy_contents(source, destination, continue_on_error: false)
|
49
|
+
[ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error)]
|
50
|
+
end
|
51
|
+
end
|