kamal 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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