kamal 0.16.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1021 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +239 -0
  6. data/lib/kamal/cli/app.rb +296 -0
  7. data/lib/kamal/cli/base.rb +171 -0
  8. data/lib/kamal/cli/build.rb +106 -0
  9. data/lib/kamal/cli/healthcheck.rb +20 -0
  10. data/lib/kamal/cli/lock.rb +37 -0
  11. data/lib/kamal/cli/main.rb +249 -0
  12. data/lib/kamal/cli/prune.rb +30 -0
  13. data/lib/kamal/cli/registry.rb +18 -0
  14. data/lib/kamal/cli/server.rb +21 -0
  15. data/lib/kamal/cli/templates/deploy.yml +74 -0
  16. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  17. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  18. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  19. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
  20. data/lib/kamal/cli/templates/template.env +2 -0
  21. data/lib/kamal/cli/traefik.rb +111 -0
  22. data/lib/kamal/cli.rb +7 -0
  23. data/lib/kamal/commander.rb +154 -0
  24. data/lib/kamal/commands/accessory.rb +113 -0
  25. data/lib/kamal/commands/app.rb +175 -0
  26. data/lib/kamal/commands/auditor.rb +28 -0
  27. data/lib/kamal/commands/base.rb +65 -0
  28. data/lib/kamal/commands/builder/base.rb +60 -0
  29. data/lib/kamal/commands/builder/multiarch/remote.rb +51 -0
  30. data/lib/kamal/commands/builder/multiarch.rb +29 -0
  31. data/lib/kamal/commands/builder/native/cached.rb +16 -0
  32. data/lib/kamal/commands/builder/native/remote.rb +59 -0
  33. data/lib/kamal/commands/builder/native.rb +20 -0
  34. data/lib/kamal/commands/builder.rb +62 -0
  35. data/lib/kamal/commands/docker.rb +21 -0
  36. data/lib/kamal/commands/healthcheck.rb +57 -0
  37. data/lib/kamal/commands/hook.rb +14 -0
  38. data/lib/kamal/commands/lock.rb +63 -0
  39. data/lib/kamal/commands/prune.rb +38 -0
  40. data/lib/kamal/commands/registry.rb +20 -0
  41. data/lib/kamal/commands/traefik.rb +104 -0
  42. data/lib/kamal/commands.rb +2 -0
  43. data/lib/kamal/configuration/accessory.rb +169 -0
  44. data/lib/kamal/configuration/boot.rb +20 -0
  45. data/lib/kamal/configuration/builder.rb +114 -0
  46. data/lib/kamal/configuration/role.rb +155 -0
  47. data/lib/kamal/configuration/ssh.rb +38 -0
  48. data/lib/kamal/configuration/sshkit.rb +20 -0
  49. data/lib/kamal/configuration.rb +251 -0
  50. data/lib/kamal/sshkit_with_ext.rb +104 -0
  51. data/lib/kamal/tags.rb +39 -0
  52. data/lib/kamal/utils/healthcheck_poller.rb +39 -0
  53. data/lib/kamal/utils/sensitive.rb +19 -0
  54. data/lib/kamal/utils.rb +100 -0
  55. data/lib/kamal/version.rb +3 -0
  56. data/lib/kamal.rb +10 -0
  57. metadata +266 -0
@@ -0,0 +1,171 @@
1
+ require "thor"
2
+ require "dotenv"
3
+ require "kamal/sshkit_with_ext"
4
+
5
+ module Kamal::Cli
6
+ class Base < Thor
7
+ include SSHKit::DSL
8
+
9
+ def self.exit_on_failure?() true end
10
+
11
+ class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
12
+ class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
13
+
14
+ class_option :version, desc: "Run commands against a specific app version"
15
+
16
+ class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
17
+ class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)"
18
+ class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)"
19
+
20
+ class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
21
+ class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
22
+
23
+ class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
24
+
25
+ def initialize(*)
26
+ super
27
+ load_envs
28
+ initialize_commander(options_with_subcommand_class_options)
29
+ end
30
+
31
+ private
32
+ def load_envs
33
+ if destination = options[:destination]
34
+ Dotenv.load(".env.#{destination}", ".env")
35
+ else
36
+ Dotenv.load(".env")
37
+ end
38
+ end
39
+
40
+ def options_with_subcommand_class_options
41
+ options.merge(@_initializer.last[:class_options] || {})
42
+ end
43
+
44
+ def initialize_commander(options)
45
+ KAMAL.tap do |commander|
46
+ if options[:verbose]
47
+ ENV["VERBOSE"] = "1" # For backtraces via cli/start
48
+ commander.verbosity = :debug
49
+ end
50
+
51
+ if options[:quiet]
52
+ commander.verbosity = :error
53
+ end
54
+
55
+ commander.configure \
56
+ config_file: Pathname.new(File.expand_path(options[:config_file])),
57
+ destination: options[:destination],
58
+ version: options[:version]
59
+
60
+ commander.specific_hosts = options[:hosts]&.split(",")
61
+ commander.specific_roles = options[:roles]&.split(",")
62
+ commander.specific_primary! if options[:primary]
63
+ end
64
+ end
65
+
66
+ def print_runtime
67
+ started_at = Time.now
68
+ yield
69
+ return Time.now - started_at
70
+ ensure
71
+ runtime = Time.now - started_at
72
+ puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
73
+ end
74
+
75
+ def mutating
76
+ return yield if KAMAL.holding_lock?
77
+
78
+ KAMAL.config.ensure_env_available
79
+
80
+ run_hook "pre-connect"
81
+
82
+ acquire_lock
83
+
84
+ begin
85
+ yield
86
+ rescue
87
+ if KAMAL.hold_lock_on_error?
88
+ error " \e[31mDeploy lock was not released\e[0m"
89
+ else
90
+ release_lock
91
+ end
92
+
93
+ raise
94
+ end
95
+
96
+ release_lock
97
+ end
98
+
99
+ def acquire_lock
100
+ raise_if_locked do
101
+ say "Acquiring the deploy lock...", :magenta
102
+ on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
103
+ end
104
+
105
+ KAMAL.holding_lock = true
106
+ end
107
+
108
+ def release_lock
109
+ say "Releasing the deploy lock...", :magenta
110
+ on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
111
+
112
+ KAMAL.holding_lock = false
113
+ end
114
+
115
+ def raise_if_locked
116
+ yield
117
+ rescue SSHKit::Runner::ExecuteError => e
118
+ if e.message =~ /cannot create directory/
119
+ on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
120
+ raise LockError, "Deploy lock found"
121
+ else
122
+ raise e
123
+ end
124
+ end
125
+
126
+ def hold_lock_on_error
127
+ if KAMAL.hold_lock_on_error?
128
+ yield
129
+ else
130
+ KAMAL.hold_lock_on_error = true
131
+ yield
132
+ KAMAL.hold_lock_on_error = false
133
+ end
134
+ end
135
+
136
+ def run_hook(hook, **extra_details)
137
+ if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
138
+ details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
139
+
140
+ say "Running the #{hook} hook...", :magenta
141
+ run_locally do
142
+ KAMAL.with_verbosity(:debug) { execute *KAMAL.hook.run(hook, **details, **extra_details) }
143
+ rescue SSHKit::Command::Failed
144
+ raise HookError.new("Hook `#{hook}` failed")
145
+ end
146
+ end
147
+ end
148
+
149
+ def command
150
+ @kamal_command ||= begin
151
+ invocation_class, invocation_commands = *first_invocation
152
+ if invocation_class == Kamal::Cli::Main
153
+ invocation_commands[0]
154
+ else
155
+ Kamal::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
156
+ end
157
+ end
158
+ end
159
+
160
+ def subcommand
161
+ @kamal_subcommand ||= begin
162
+ invocation_class, invocation_commands = *first_invocation
163
+ invocation_commands[0] if invocation_class != Kamal::Cli::Main
164
+ end
165
+ end
166
+
167
+ def first_invocation
168
+ instance_variable_get("@_invocations").first
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,106 @@
1
+ class Kamal::Cli::Build < Kamal::Cli::Base
2
+ class BuildError < StandardError; end
3
+
4
+ desc "deliver", "Build app and push app image to registry then pull image on servers"
5
+ def deliver
6
+ mutating do
7
+ push
8
+ pull
9
+ end
10
+ end
11
+
12
+ desc "push", "Build and push app image to registry"
13
+ def push
14
+ mutating do
15
+ cli = self
16
+
17
+ verify_local_dependencies
18
+ run_hook "pre-build"
19
+
20
+ if (uncommitted_changes = Kamal::Utils.uncommitted_changes).present?
21
+ say "The following paths have uncommitted changes:\n #{uncommitted_changes}", :yellow
22
+ end
23
+
24
+ run_locally do
25
+ begin
26
+ KAMAL.with_verbosity(:debug) do
27
+ execute *KAMAL.builder.push
28
+ end
29
+ rescue SSHKit::Command::Failed => e
30
+ if e.message =~ /(no builder)|(no such file or directory)/
31
+ error "Missing compatible builder, so creating a new one first"
32
+
33
+ if cli.create
34
+ KAMAL.with_verbosity(:debug) { execute *KAMAL.builder.push }
35
+ end
36
+ else
37
+ raise
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ desc "pull", "Pull app image from registry onto servers"
45
+ def pull
46
+ mutating do
47
+ on(KAMAL.hosts) do
48
+ execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
49
+ execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
50
+ execute *KAMAL.builder.pull
51
+ end
52
+ end
53
+ end
54
+
55
+ desc "create", "Create a build setup"
56
+ def create
57
+ mutating do
58
+ run_locally do
59
+ begin
60
+ debug "Using builder: #{KAMAL.builder.name}"
61
+ execute *KAMAL.builder.create
62
+ rescue SSHKit::Command::Failed => e
63
+ if e.message =~ /stderr=(.*)/
64
+ error "Couldn't create remote builder: #{$1}"
65
+ false
66
+ else
67
+ raise
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ desc "remove", "Remove build setup"
75
+ def remove
76
+ mutating do
77
+ run_locally do
78
+ debug "Using builder: #{KAMAL.builder.name}"
79
+ execute *KAMAL.builder.remove
80
+ end
81
+ end
82
+ end
83
+
84
+ desc "details", "Show build setup"
85
+ def details
86
+ run_locally do
87
+ puts "Builder: #{KAMAL.builder.name}"
88
+ puts capture(*KAMAL.builder.info)
89
+ end
90
+ end
91
+
92
+ private
93
+ def verify_local_dependencies
94
+ run_locally do
95
+ begin
96
+ execute *KAMAL.builder.ensure_local_dependencies_installed
97
+ rescue SSHKit::Command::Failed => e
98
+ build_error = e.message =~ /command not found/ ?
99
+ "Docker is not installed locally" :
100
+ "Docker buildx plugin is not installed locally"
101
+
102
+ raise BuildError, build_error
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,20 @@
1
+ class Kamal::Cli::Healthcheck < Kamal::Cli::Base
2
+ default_command :perform
3
+
4
+ desc "perform", "Health check current app version"
5
+ def perform
6
+ on(KAMAL.primary_host) do
7
+ begin
8
+ execute *KAMAL.healthcheck.run
9
+ Kamal::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*KAMAL.healthcheck.status) }
10
+ rescue Kamal::Utils::HealthcheckPoller::HealthcheckError => e
11
+ error capture_with_info(*KAMAL.healthcheck.logs)
12
+ error capture_with_pretty_json(*KAMAL.healthcheck.container_health_log)
13
+ raise
14
+ ensure
15
+ execute *KAMAL.healthcheck.stop, raise_on_non_zero_exit: false
16
+ execute *KAMAL.healthcheck.remove, raise_on_non_zero_exit: false
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
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) { puts capture_with_debug(*KAMAL.lock.status) }
6
+ end
7
+ end
8
+
9
+ desc "acquire", "Acquire the deploy lock"
10
+ option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
11
+ def acquire
12
+ message = options[:message]
13
+ raise_if_locked do
14
+ on(KAMAL.primary_host) { execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug }
15
+ say "Acquired the deploy lock"
16
+ end
17
+ end
18
+
19
+ desc "release", "Release the deploy lock"
20
+ def release
21
+ handle_missing_lock do
22
+ on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
23
+ say "Released the deploy lock"
24
+ end
25
+ end
26
+
27
+ private
28
+ def handle_missing_lock
29
+ yield
30
+ rescue SSHKit::Runner::ExecuteError => e
31
+ if e.message =~ /No such file or directory/
32
+ say "There is no deploy lock"
33
+ else
34
+ raise
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,249 @@
1
+ class Kamal::Cli::Main < Kamal::Cli::Base
2
+ desc "setup", "Setup all accessories and deploy app to servers"
3
+ def setup
4
+ print_runtime do
5
+ mutating do
6
+ invoke "kamal:cli:server:bootstrap"
7
+ invoke "kamal:cli:accessory:boot", [ "all" ]
8
+ deploy
9
+ end
10
+ end
11
+ end
12
+
13
+ desc "deploy", "Deploy app to servers"
14
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
15
+ def deploy
16
+ runtime = print_runtime do
17
+ mutating do
18
+ invoke_options = deploy_options
19
+
20
+ say "Log into image registry...", :magenta
21
+ invoke "kamal:cli:registry:login", [], invoke_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
+ run_hook "pre-deploy"
32
+
33
+ say "Ensure Traefik is running...", :magenta
34
+ invoke "kamal:cli:traefik:boot", [], invoke_options
35
+
36
+ say "Ensure app can pass healthcheck...", :magenta
37
+ invoke "kamal:cli:healthcheck:perform", [], invoke_options
38
+
39
+ say "Detect stale containers...", :magenta
40
+ invoke "kamal:cli:app:stale_containers", [], invoke_options
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", runtime: runtime.round
50
+ end
51
+
52
+ desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
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
+ mutating do
57
+ invoke_options = deploy_options
58
+
59
+ if options[:skip_push]
60
+ say "Pull app image...", :magenta
61
+ invoke "kamal:cli:build:pull", [], invoke_options
62
+ else
63
+ say "Build and push app image...", :magenta
64
+ invoke "kamal:cli:build:deliver", [], invoke_options
65
+ end
66
+
67
+ run_hook "pre-deploy"
68
+
69
+ say "Ensure app can pass healthcheck...", :magenta
70
+ invoke "kamal:cli:healthcheck:perform", [], invoke_options
71
+
72
+ say "Detect stale containers...", :magenta
73
+ invoke "kamal:cli:app:stale_containers", [], invoke_options
74
+
75
+ invoke "kamal:cli:app:boot", [], invoke_options
76
+ end
77
+ end
78
+
79
+ run_hook "post-deploy", runtime: runtime.round
80
+ end
81
+
82
+ desc "rollback [VERSION]", "Rollback app to VERSION"
83
+ def rollback(version)
84
+ rolled_back = false
85
+ runtime = print_runtime do
86
+ mutating do
87
+ invoke_options = deploy_options
88
+
89
+ KAMAL.config.version = version
90
+ old_version = nil
91
+
92
+ if container_available?(version)
93
+ run_hook "pre-deploy"
94
+
95
+ invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
96
+ rolled_back = true
97
+ else
98
+ say "The app version '#{version}' is not available as a container (use 'kamal app containers' for available versions)", :red
99
+ end
100
+ end
101
+ end
102
+
103
+ run_hook "post-deploy", runtime: runtime.round if rolled_back
104
+ end
105
+
106
+ desc "details", "Show details about all containers"
107
+ def details
108
+ invoke "kamal:cli:traefik:details"
109
+ invoke "kamal:cli:app:details"
110
+ invoke "kamal:cli:accessory:details", [ "all" ]
111
+ end
112
+
113
+ desc "audit", "Show audit log from servers"
114
+ def audit
115
+ on(KAMAL.hosts) do |host|
116
+ puts_by_host host, capture_with_info(*KAMAL.auditor.reveal)
117
+ end
118
+ end
119
+
120
+ desc "config", "Show combined config (including secrets!)"
121
+ def config
122
+ run_locally do
123
+ puts Kamal::Utils.redacted(KAMAL.config.to_h).to_yaml
124
+ end
125
+ end
126
+
127
+ desc "init", "Create config stub in config/deploy.yml and env stub in .env"
128
+ option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
129
+ def init
130
+ require "fileutils"
131
+
132
+ if (deploy_file = Pathname.new(File.expand_path("config/deploy.yml"))).exist?
133
+ puts "Config file already exists in config/deploy.yml (remove first to create a new one)"
134
+ else
135
+ FileUtils.mkdir_p deploy_file.dirname
136
+ FileUtils.cp_r Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file
137
+ puts "Created configuration file in config/deploy.yml"
138
+ end
139
+
140
+ unless (deploy_file = Pathname.new(File.expand_path(".env"))).exist?
141
+ FileUtils.cp_r Pathname.new(File.expand_path("templates/template.env", __dir__)), deploy_file
142
+ puts "Created .env file"
143
+ end
144
+
145
+ unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
146
+ hooks_dir.mkpath
147
+ Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
148
+ FileUtils.cp sample_hook, hooks_dir, preserve: true
149
+ end
150
+ puts "Created sample hooks in .kamal/hooks"
151
+ end
152
+
153
+ if options[:bundle]
154
+ if (binstub = Pathname.new(File.expand_path("bin/kamal"))).exist?
155
+ puts "Binstub already exists in bin/kamal (remove first to create a new one)"
156
+ else
157
+ puts "Adding Kamal to Gemfile and bundle..."
158
+ run_locally do
159
+ execute :bundle, :add, :kamal
160
+ execute :bundle, :binstubs, :kamal
161
+ end
162
+ puts "Created binstub file in bin/kamal"
163
+ end
164
+ end
165
+ end
166
+
167
+ desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
168
+ def envify
169
+ if destination = options[:destination]
170
+ env_template_path = ".env.#{destination}.erb"
171
+ env_path = ".env.#{destination}"
172
+ else
173
+ env_template_path = ".env.erb"
174
+ env_path = ".env"
175
+ end
176
+
177
+ File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
178
+ end
179
+
180
+ desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
181
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
182
+ def remove
183
+ mutating do
184
+ if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
185
+ invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
186
+ invoke "kamal:cli:app:remove", [], options.without(:confirmed)
187
+ invoke "kamal:cli:accessory:remove", [ "all" ], options
188
+ invoke "kamal:cli:registry:logout", [], options.without(:confirmed)
189
+ end
190
+ end
191
+ end
192
+
193
+ desc "version", "Show Kamal version"
194
+ def version
195
+ puts Kamal::VERSION
196
+ end
197
+
198
+ desc "accessory", "Manage accessories (db/redis/search)"
199
+ subcommand "accessory", Kamal::Cli::Accessory
200
+
201
+ desc "app", "Manage application"
202
+ subcommand "app", Kamal::Cli::App
203
+
204
+ desc "build", "Build application image"
205
+ subcommand "build", Kamal::Cli::Build
206
+
207
+ desc "healthcheck", "Healthcheck application"
208
+ subcommand "healthcheck", Kamal::Cli::Healthcheck
209
+
210
+ desc "lock", "Manage the deploy lock"
211
+ subcommand "lock", Kamal::Cli::Lock
212
+
213
+ desc "prune", "Prune old application images and containers"
214
+ subcommand "prune", Kamal::Cli::Prune
215
+
216
+ desc "registry", "Login and -out of the image registry"
217
+ subcommand "registry", Kamal::Cli::Registry
218
+
219
+ desc "server", "Bootstrap servers with curl and Docker"
220
+ subcommand "server", Kamal::Cli::Server
221
+
222
+ desc "traefik", "Manage Traefik load balancer"
223
+ subcommand "traefik", Kamal::Cli::Traefik
224
+
225
+ private
226
+ def container_available?(version)
227
+ begin
228
+ on(KAMAL.hosts) do
229
+ KAMAL.roles_on(host).each do |role|
230
+ container_id = capture_with_info(*KAMAL.app(role: role).container_id_for_version(version))
231
+ raise "Container not found" unless container_id.present?
232
+ end
233
+ end
234
+ rescue SSHKit::Runner::ExecuteError => e
235
+ if e.message =~ /Container not found/
236
+ say "Error looking for container version #{version}: #{e.message}"
237
+ return false
238
+ else
239
+ raise
240
+ end
241
+ end
242
+
243
+ true
244
+ end
245
+
246
+ def deploy_options
247
+ { "version" => KAMAL.config.version }.merge(options.without("skip_push"))
248
+ end
249
+ end
@@ -0,0 +1,30 @@
1
+ class Kamal::Cli::Prune < Kamal::Cli::Base
2
+ desc "all", "Prune unused images and stopped containers"
3
+ def all
4
+ mutating do
5
+ containers
6
+ images
7
+ end
8
+ end
9
+
10
+ desc "images", "Prune dangling images"
11
+ def images
12
+ mutating 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 5"
22
+ def containers
23
+ mutating do
24
+ on(KAMAL.hosts) do
25
+ execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
26
+ execute *KAMAL.prune.containers
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ class Kamal::Cli::Registry < Kamal::Cli::Base
2
+ desc "login", "Log in to registry locally and remotely"
3
+ def login
4
+ run_locally { execute *KAMAL.registry.login }
5
+ on(KAMAL.hosts) { execute *KAMAL.registry.login }
6
+ # FIXME: This rescue needed?
7
+ rescue ArgumentError => e
8
+ puts e.message
9
+ end
10
+
11
+ desc "logout", "Log out of registry remotely"
12
+ def logout
13
+ on(KAMAL.hosts) { execute *KAMAL.registry.logout }
14
+ # FIXME: This rescue needed?
15
+ rescue ArgumentError => e
16
+ puts e.message
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ class Kamal::Cli::Server < Kamal::Cli::Base
2
+ desc "bootstrap", "Set up Docker to run Kamal apps"
3
+ def bootstrap
4
+ missing = []
5
+
6
+ on(KAMAL.hosts | KAMAL.accessory_hosts) do |host|
7
+ unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
8
+ if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
9
+ info "Missing Docker on #{host}. Installing…"
10
+ execute *KAMAL.docker.install
11
+ else
12
+ missing << host
13
+ end
14
+ end
15
+ end
16
+
17
+ if missing.any?
18
+ raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
19
+ end
20
+ end
21
+ end