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,204 @@
1
+ require "uri"
2
+
3
+ class Kamal::Cli::Build < Kamal::Cli::Base
4
+ class BuildError < StandardError; end
5
+
6
+ desc "deliver", "Build app and push app image to registry then pull image on servers"
7
+ def deliver
8
+ invoke :push
9
+ invoke :pull
10
+ end
11
+
12
+ desc "push", "Build and push app image to registry"
13
+ option :output, type: :string, default: "registry", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
14
+ def push
15
+ cli = self
16
+
17
+ # Ensure pre-connect hooks run before the build, they may needed for a remote builder
18
+ # or the pre-build hooks.
19
+ pre_connect_if_required
20
+
21
+ ensure_docker_installed
22
+ login_to_registry_locally
23
+
24
+ run_hook "pre-build"
25
+
26
+ uncommitted_changes = Kamal::Git.uncommitted_changes
27
+
28
+ if KAMAL.config.builder.git_clone?
29
+ if uncommitted_changes.present?
30
+ say "Building from a local git clone, so ignoring these uncommitted changes:\n #{uncommitted_changes}", :yellow
31
+ end
32
+
33
+ run_locally do
34
+ Clone.new(self).prepare
35
+ end
36
+ elsif uncommitted_changes.present?
37
+ say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
38
+ end
39
+
40
+ with_env(KAMAL.config.builder.secrets) do
41
+ run_locally do
42
+ begin
43
+ execute *KAMAL.builder.inspect_builder
44
+ rescue SSHKit::Command::Failed => e
45
+ if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
46
+ warn "Missing compatible builder, so creating a new one first"
47
+ begin
48
+ cli.remove
49
+ rescue SSHKit::Command::Failed
50
+ raise unless e.message =~ /(context not found|no builder|does not exist)/
51
+ end
52
+ cli.create
53
+ else
54
+ raise
55
+ end
56
+ end
57
+
58
+ # Get the command here to ensure the Dir.chdir doesn't interfere with it
59
+ push = KAMAL.builder.push(cli.options[:output])
60
+
61
+ KAMAL.with_verbosity(:debug) do
62
+ Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ desc "pull", "Pull app image from registry onto servers"
69
+ def pull
70
+ login_to_registry_remotely
71
+
72
+ if (first_hosts = mirror_hosts).any?
73
+ #  Pull on a single host per mirror first to seed them
74
+ say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
75
+ pull_on_hosts(first_hosts)
76
+ say "Pulling image on remaining hosts...", :magenta
77
+ pull_on_hosts(KAMAL.app_hosts - first_hosts)
78
+ else
79
+ pull_on_hosts(KAMAL.app_hosts)
80
+ end
81
+ end
82
+
83
+ desc "create", "Create a build setup"
84
+ def create
85
+ if (remote_host = KAMAL.config.builder.remote)
86
+ connect_to_remote_host(remote_host)
87
+ end
88
+
89
+ run_locally do
90
+ begin
91
+ debug "Using builder: #{KAMAL.builder.name}"
92
+ execute *KAMAL.builder.create
93
+ rescue SSHKit::Command::Failed => e
94
+ if e.message =~ /stderr=(.*)/
95
+ error "Couldn't create remote builder: #{$1}"
96
+ false
97
+ else
98
+ raise
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ desc "remove", "Remove build setup"
105
+ def remove
106
+ run_locally do
107
+ debug "Using builder: #{KAMAL.builder.name}"
108
+ execute *KAMAL.builder.remove
109
+ end
110
+ end
111
+
112
+ desc "details", "Show build setup"
113
+ def details
114
+ run_locally do
115
+ puts "Builder: #{KAMAL.builder.name}"
116
+ puts capture(*KAMAL.builder.info)
117
+ end
118
+ end
119
+
120
+ desc "dev", "Build using the working directory, tag it as dirty, and push to local image store."
121
+ option :output, type: :string, default: "docker", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
122
+ def dev
123
+ cli = self
124
+
125
+ ensure_docker_installed
126
+
127
+ docker_included_files = Set.new(Kamal::Docker.included_files)
128
+ git_uncommitted_files = Set.new(Kamal::Git.uncommitted_files)
129
+ git_untracked_files = Set.new(Kamal::Git.untracked_files)
130
+
131
+ docker_uncommitted_files = docker_included_files & git_uncommitted_files
132
+ if docker_uncommitted_files.any?
133
+ say "WARNING: Files with uncommitted changes will be present in the dev container:", :yellow
134
+ docker_uncommitted_files.sort.each { |f| say " #{f}", :yellow }
135
+ say
136
+ end
137
+
138
+ docker_untracked_files = docker_included_files & git_untracked_files
139
+ if docker_untracked_files.any?
140
+ say "WARNING: Untracked files will be present in the dev container:", :yellow
141
+ docker_untracked_files.sort.each { |f| say " #{f}", :yellow }
142
+ say
143
+ end
144
+
145
+ with_env(KAMAL.config.builder.secrets) do
146
+ run_locally do
147
+ build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true)
148
+ KAMAL.with_verbosity(:debug) do
149
+ execute(*build)
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ private
156
+ def connect_to_remote_host(remote_host)
157
+ remote_uri = URI.parse(remote_host)
158
+ if remote_uri.scheme == "ssh"
159
+ host = SSHKit::Host.new(
160
+ hostname: remote_uri.host,
161
+ ssh_options: { user: remote_uri.user, port: remote_uri.port }.compact
162
+ )
163
+ on(host, options) do
164
+ execute "true"
165
+ end
166
+ end
167
+ end
168
+
169
+ def mirror_hosts
170
+ if KAMAL.app_hosts.many?
171
+ mirror_hosts = Concurrent::Hash.new
172
+ on(KAMAL.app_hosts) do |host|
173
+ first_mirror = capture_with_info(*KAMAL.builder.first_mirror).strip.presence
174
+ mirror_hosts[first_mirror] ||= host.to_s if first_mirror
175
+ rescue SSHKit::Command::Failed => e
176
+ raise unless e.message =~ /error calling index: reflect: slice index out of range/
177
+ end
178
+ mirror_hosts.values
179
+ else
180
+ []
181
+ end
182
+ end
183
+
184
+ def pull_on_hosts(hosts)
185
+ on(hosts) do
186
+ execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
187
+ execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
188
+ execute *KAMAL.builder.pull
189
+ execute *KAMAL.builder.validate_image
190
+ end
191
+ end
192
+
193
+ def login_to_registry_locally
194
+ run_locally do
195
+ execute *KAMAL.registry.login
196
+ end
197
+ end
198
+
199
+ def login_to_registry_remotely
200
+ on(KAMAL.app_hosts) do
201
+ execute *KAMAL.registry.login
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,33 @@
1
+ require "concurrent/ivar"
2
+
3
+ class Kamal::Cli::Healthcheck::Barrier
4
+ def initialize
5
+ @ivar = Concurrent::IVar.new
6
+ end
7
+
8
+ def close
9
+ set(false)
10
+ end
11
+
12
+ def open
13
+ set(true)
14
+ end
15
+
16
+ def wait
17
+ unless opened?
18
+ raise Kamal::Cli::Healthcheck::Error.new("Halted at barrier")
19
+ end
20
+ end
21
+
22
+ private
23
+ def opened?
24
+ @ivar.value
25
+ end
26
+
27
+ def set(value)
28
+ @ivar.set(value)
29
+ true
30
+ rescue Concurrent::MultipleAssignmentError
31
+ false
32
+ end
33
+ end
@@ -0,0 +1,2 @@
1
+ class Kamal::Cli::Healthcheck::Error < StandardError
2
+ end
@@ -0,0 +1,42 @@
1
+ module Kamal::Cli::Healthcheck::Poller
2
+ extend self
3
+
4
+ def wait_for_healthy(role, &block)
5
+ attempt = 1
6
+ timeout_at = Time.now + KAMAL.config.deploy_timeout
7
+ readiness_delay = KAMAL.config.readiness_delay
8
+
9
+ begin
10
+ status = block.call
11
+
12
+ if status == "running"
13
+ # Wait for the readiness delay and confirm it is still running
14
+ if readiness_delay > 0
15
+ info "Container is running, waiting for readiness delay of #{readiness_delay} seconds"
16
+ sleep readiness_delay
17
+ status = block.call
18
+ end
19
+ end
20
+
21
+ unless %w[ running healthy ].include?(status)
22
+ raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.deploy_timeout} seconds (#{status})"
23
+ end
24
+ rescue Kamal::Cli::Healthcheck::Error => e
25
+ time_left = timeout_at - Time.now
26
+ if time_left > 0
27
+ sleep [ attempt, time_left ].min
28
+ attempt += 1
29
+ retry
30
+ else
31
+ raise
32
+ end
33
+ end
34
+
35
+ info "Container is healthy!"
36
+ end
37
+
38
+ private
39
+ def info(message)
40
+ SSHKit.config.output.info(message)
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ class Kamal::Cli::Lock < Kamal::Cli::Base
2
+ desc "status", "Report lock status"
3
+ def status
4
+ handle_missing_lock do
5
+ on(KAMAL.primary_host) do
6
+ puts capture_with_debug(*KAMAL.lock.status)
7
+ end
8
+ end
9
+ end
10
+
11
+ desc "acquire", "Acquire the deploy lock"
12
+ option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
13
+ def acquire
14
+ message = options[:message]
15
+ ensure_run_directory
16
+
17
+ raise_if_locked do
18
+ on(KAMAL.primary_host) do
19
+ execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
20
+ end
21
+ say "Acquired the deploy lock"
22
+ end
23
+ end
24
+
25
+ desc "release", "Release the deploy lock"
26
+ def release
27
+ handle_missing_lock do
28
+ on(KAMAL.primary_host) do
29
+ execute *KAMAL.lock.release, verbosity: :debug
30
+ end
31
+ say "Released the deploy lock"
32
+ end
33
+ end
34
+
35
+ private
36
+ def handle_missing_lock
37
+ yield
38
+ rescue SSHKit::Runner::ExecuteError => e
39
+ if e.message =~ /No such file or directory/
40
+ say "There is no deploy lock"
41
+ else
42
+ raise
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,277 @@
1
+ class Kamal::Cli::Main < Kamal::Cli::Base
2
+ desc "setup", "Setup all accessories, push the env, and deploy app to servers"
3
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
4
+ def setup
5
+ print_runtime do
6
+ with_lock do
7
+ invoke_options = deploy_options
8
+
9
+ say "Ensure Docker is installed...", :magenta
10
+ invoke "kamal:cli:server:bootstrap", [], invoke_options
11
+
12
+ deploy(boot_accessories: true)
13
+ end
14
+ end
15
+ end
16
+
17
+ desc "deploy", "Deploy app to servers"
18
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
19
+ def deploy(boot_accessories: false)
20
+ runtime = print_runtime do
21
+ invoke_options = deploy_options
22
+
23
+ if options[:skip_push]
24
+ say "Pull app image...", :magenta
25
+ invoke "kamal:cli:build:pull", [], invoke_options
26
+ else
27
+ say "Build and push app image...", :magenta
28
+ invoke "kamal:cli:build:deliver", [], invoke_options
29
+ end
30
+
31
+ with_lock do
32
+ run_hook "pre-deploy", secrets: true
33
+
34
+ say "Ensure kamal-proxy is running...", :magenta
35
+ invoke "kamal:cli:proxy:boot", [], invoke_options
36
+
37
+ invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options if boot_accessories
38
+
39
+ say "Detect stale containers...", :magenta
40
+ invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
41
+
42
+ invoke "kamal:cli:app:boot", [], invoke_options
43
+
44
+ say "Prune old containers and images...", :magenta
45
+ invoke "kamal:cli:prune:all", [], invoke_options
46
+ end
47
+ end
48
+
49
+ run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
50
+ end
51
+
52
+ desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy and pruning"
53
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
54
+ def redeploy
55
+ runtime = print_runtime do
56
+ invoke_options = deploy_options
57
+
58
+ if options[:skip_push]
59
+ say "Pull app image...", :magenta
60
+ invoke "kamal:cli:build:pull", [], invoke_options
61
+ else
62
+ say "Build and push app image...", :magenta
63
+ invoke "kamal:cli:build:deliver", [], invoke_options
64
+ end
65
+
66
+ with_lock do
67
+ run_hook "pre-deploy", secrets: true
68
+
69
+ say "Detect stale containers...", :magenta
70
+ invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
71
+
72
+ invoke "kamal:cli:app:boot", [], invoke_options
73
+ end
74
+ end
75
+
76
+ run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
77
+ end
78
+
79
+ desc "rollback [VERSION]", "Rollback app to VERSION"
80
+ def rollback(version)
81
+ rolled_back = false
82
+ runtime = print_runtime do
83
+ with_lock do
84
+ invoke_options = deploy_options
85
+
86
+ KAMAL.config.version = version
87
+ old_version = nil
88
+
89
+ if container_available?(version)
90
+ run_hook "pre-deploy", secrets: true
91
+
92
+ invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
93
+ rolled_back = true
94
+ else
95
+ say "The app version '#{version}' is not available as a container (use 'kamal app containers' for available versions)", :red
96
+ end
97
+ end
98
+ end
99
+
100
+ run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
101
+ end
102
+
103
+ desc "details", "Show details about all containers"
104
+ def details
105
+ invoke "kamal:cli:proxy:details"
106
+ invoke "kamal:cli:app:details"
107
+ invoke "kamal:cli:accessory:details", [ "all" ]
108
+ end
109
+
110
+ desc "audit", "Show audit log from servers"
111
+ def audit
112
+ on(KAMAL.hosts) do |host|
113
+ puts_by_host host, capture_with_info(*KAMAL.auditor.reveal)
114
+ end
115
+ end
116
+
117
+ desc "config", "Show combined config (including secrets!)"
118
+ def config
119
+ run_locally do
120
+ puts Kamal::Utils.redacted(KAMAL.config.to_h).to_yaml
121
+ end
122
+ end
123
+
124
+ desc "docs [SECTION]", "Show Kamal configuration documentation"
125
+ def docs(section = nil)
126
+ case section
127
+ when NilClass
128
+ puts Kamal::Configuration.validation_doc
129
+ else
130
+ puts Kamal::Configuration.const_get(section.titlecase.to_sym).validation_doc
131
+ end
132
+ rescue NameError
133
+ puts "No documentation found for #{section}"
134
+ end
135
+
136
+ desc "init", "Create config stub in config/deploy.yml and secrets stub in .kamal"
137
+ option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
138
+ def init
139
+ require "fileutils"
140
+
141
+ if (deploy_file = Pathname.new(File.expand_path("config/deploy.yml"))).exist?
142
+ puts "Config file already exists in config/deploy.yml (remove first to create a new one)"
143
+ else
144
+ FileUtils.mkdir_p deploy_file.dirname
145
+ FileUtils.cp_r Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file
146
+ puts "Created configuration file in config/deploy.yml"
147
+ end
148
+
149
+ unless (secrets_file = Pathname.new(File.expand_path(".kamal/secrets"))).exist?
150
+ FileUtils.mkdir_p secrets_file.dirname
151
+ FileUtils.cp_r Pathname.new(File.expand_path("templates/secrets", __dir__)), secrets_file
152
+ puts "Created .kamal/secrets file"
153
+ end
154
+
155
+ unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
156
+ hooks_dir.mkpath
157
+ Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
158
+ FileUtils.cp sample_hook, hooks_dir, preserve: true
159
+ end
160
+ puts "Created sample hooks in .kamal/hooks"
161
+ end
162
+
163
+ if options[:bundle]
164
+ if (binstub = Pathname.new(File.expand_path("bin/kamal"))).exist?
165
+ puts "Binstub already exists in bin/kamal (remove first to create a new one)"
166
+ else
167
+ puts "Adding Kamal to Gemfile and bundle..."
168
+ run_locally do
169
+ execute :bundle, :add, :kamal
170
+ execute :bundle, :binstubs, :kamal
171
+ end
172
+ puts "Created binstub file in bin/kamal"
173
+ end
174
+ end
175
+ end
176
+
177
+ desc "remove", "Remove kamal-proxy, app, accessories, and registry session from servers"
178
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
179
+ def remove
180
+ confirming "This will remove all containers and images. Are you sure?" do
181
+ with_lock do
182
+ invoke "kamal:cli:app:remove", [], options.without(:confirmed)
183
+ invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
184
+ invoke "kamal:cli:accessory:remove", [ "all" ], options
185
+ invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
186
+ end
187
+ end
188
+ end
189
+
190
+ desc "upgrade", "Upgrade from Kamal 1.x to 2.0"
191
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
192
+ option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
193
+ def upgrade
194
+ confirming "This will replace Traefik with kamal-proxy and restart all accessories" do
195
+ with_lock do
196
+ if options[:rolling]
197
+ KAMAL.hosts.each do |host|
198
+ KAMAL.with_specific_hosts(host) do
199
+ say "Upgrading #{host}...", :magenta
200
+ if KAMAL.app_hosts.include?(host)
201
+ invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true, rolling: false)
202
+ reset_invocation(Kamal::Cli::Proxy)
203
+ end
204
+ if KAMAL.accessory_hosts.include?(host)
205
+ invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true, rolling: false)
206
+ reset_invocation(Kamal::Cli::Accessory)
207
+ end
208
+ say "Upgraded #{host}", :magenta
209
+ end
210
+ end
211
+ else
212
+ say "Upgrading all hosts...", :magenta
213
+ invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true)
214
+ invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true)
215
+ say "Upgraded all hosts", :magenta
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ desc "version", "Show Kamal version"
222
+ def version
223
+ puts Kamal::VERSION
224
+ end
225
+
226
+ desc "accessory", "Manage accessories (db/redis/search)"
227
+ subcommand "accessory", Kamal::Cli::Accessory
228
+
229
+ desc "app", "Manage application"
230
+ subcommand "app", Kamal::Cli::App
231
+
232
+ desc "build", "Build application image"
233
+ subcommand "build", Kamal::Cli::Build
234
+
235
+ desc "lock", "Manage the deploy lock"
236
+ subcommand "lock", Kamal::Cli::Lock
237
+
238
+ desc "proxy", "Manage kamal-proxy"
239
+ subcommand "proxy", Kamal::Cli::Proxy
240
+
241
+ desc "prune", "Prune old application images and containers"
242
+ subcommand "prune", Kamal::Cli::Prune
243
+
244
+ desc "registry", "Login and -out of the image registry"
245
+ subcommand "registry", Kamal::Cli::Registry
246
+
247
+ desc "secrets", "Helpers for extracting secrets"
248
+ subcommand "secrets", Kamal::Cli::Secrets
249
+
250
+ desc "server", "Bootstrap servers with curl and Docker"
251
+ subcommand "server", Kamal::Cli::Server
252
+
253
+ private
254
+ def container_available?(version)
255
+ begin
256
+ on(KAMAL.app_hosts) do
257
+ KAMAL.roles_on(host).each do |role|
258
+ container_id = capture_with_info(*KAMAL.app(role: role, host: host).container_id_for_version(version))
259
+ raise "Container not found" unless container_id.present?
260
+ end
261
+ end
262
+ rescue SSHKit::Runner::ExecuteError, SSHKit::Runner::MultipleExecuteError => e
263
+ if e.message =~ /Container not found/
264
+ say "Error looking for container version #{version}: #{e.message}"
265
+ return false
266
+ else
267
+ raise
268
+ end
269
+ end
270
+
271
+ true
272
+ end
273
+
274
+ def deploy_options
275
+ { "version" => KAMAL.config.version }.merge(options.without("skip_push"))
276
+ end
277
+ end