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