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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1021 -0
- data/bin/kamal +18 -0
- data/lib/kamal/cli/accessory.rb +239 -0
- data/lib/kamal/cli/app.rb +296 -0
- data/lib/kamal/cli/base.rb +171 -0
- data/lib/kamal/cli/build.rb +106 -0
- data/lib/kamal/cli/healthcheck.rb +20 -0
- data/lib/kamal/cli/lock.rb +37 -0
- data/lib/kamal/cli/main.rb +249 -0
- data/lib/kamal/cli/prune.rb +30 -0
- data/lib/kamal/cli/registry.rb +18 -0
- data/lib/kamal/cli/server.rb +21 -0
- data/lib/kamal/cli/templates/deploy.yml +74 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
- data/lib/kamal/cli/templates/template.env +2 -0
- data/lib/kamal/cli/traefik.rb +111 -0
- data/lib/kamal/cli.rb +7 -0
- data/lib/kamal/commander.rb +154 -0
- data/lib/kamal/commands/accessory.rb +113 -0
- data/lib/kamal/commands/app.rb +175 -0
- data/lib/kamal/commands/auditor.rb +28 -0
- data/lib/kamal/commands/base.rb +65 -0
- data/lib/kamal/commands/builder/base.rb +60 -0
- data/lib/kamal/commands/builder/multiarch/remote.rb +51 -0
- data/lib/kamal/commands/builder/multiarch.rb +29 -0
- data/lib/kamal/commands/builder/native/cached.rb +16 -0
- data/lib/kamal/commands/builder/native/remote.rb +59 -0
- data/lib/kamal/commands/builder/native.rb +20 -0
- data/lib/kamal/commands/builder.rb +62 -0
- data/lib/kamal/commands/docker.rb +21 -0
- data/lib/kamal/commands/healthcheck.rb +57 -0
- data/lib/kamal/commands/hook.rb +14 -0
- data/lib/kamal/commands/lock.rb +63 -0
- data/lib/kamal/commands/prune.rb +38 -0
- data/lib/kamal/commands/registry.rb +20 -0
- data/lib/kamal/commands/traefik.rb +104 -0
- data/lib/kamal/commands.rb +2 -0
- data/lib/kamal/configuration/accessory.rb +169 -0
- data/lib/kamal/configuration/boot.rb +20 -0
- data/lib/kamal/configuration/builder.rb +114 -0
- data/lib/kamal/configuration/role.rb +155 -0
- data/lib/kamal/configuration/ssh.rb +38 -0
- data/lib/kamal/configuration/sshkit.rb +20 -0
- data/lib/kamal/configuration.rb +251 -0
- data/lib/kamal/sshkit_with_ext.rb +104 -0
- data/lib/kamal/tags.rb +39 -0
- data/lib/kamal/utils/healthcheck_poller.rb +39 -0
- data/lib/kamal/utils/sensitive.rb +19 -0
- data/lib/kamal/utils.rb +100 -0
- data/lib/kamal/version.rb +3 -0
- data/lib/kamal.rb +10 -0
- 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
|