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,74 @@
|
|
1
|
+
# Name of your application. Used to uniquely configure containers.
|
2
|
+
service: my-app
|
3
|
+
|
4
|
+
# Name of the container image.
|
5
|
+
image: user/my-app
|
6
|
+
|
7
|
+
# Deploy to these servers.
|
8
|
+
servers:
|
9
|
+
- 192.168.0.1
|
10
|
+
|
11
|
+
# Credentials for your image host.
|
12
|
+
registry:
|
13
|
+
# Specify the registry server, if you're not using Docker Hub
|
14
|
+
# server: registry.digitalocean.com / ghcr.io / ...
|
15
|
+
username: my-user
|
16
|
+
|
17
|
+
# Always use an access token rather than real password when possible.
|
18
|
+
password:
|
19
|
+
- KAMAL_REGISTRY_PASSWORD
|
20
|
+
|
21
|
+
# Inject ENV variables into containers (secrets come from .env).
|
22
|
+
# env:
|
23
|
+
# clear:
|
24
|
+
# DB_HOST: 192.168.0.2
|
25
|
+
# secret:
|
26
|
+
# - RAILS_MASTER_KEY
|
27
|
+
|
28
|
+
# Use a different ssh user than root
|
29
|
+
# ssh:
|
30
|
+
# user: app
|
31
|
+
|
32
|
+
# Configure builder setup.
|
33
|
+
# builder:
|
34
|
+
# args:
|
35
|
+
# RUBY_VERSION: 3.2.0
|
36
|
+
# secrets:
|
37
|
+
# - GITHUB_TOKEN
|
38
|
+
# remote:
|
39
|
+
# arch: amd64
|
40
|
+
# host: ssh://app@192.168.0.1
|
41
|
+
|
42
|
+
# Use accessory services (secrets come from .env).
|
43
|
+
# accessories:
|
44
|
+
# db:
|
45
|
+
# image: mysql:8.0
|
46
|
+
# host: 192.168.0.2
|
47
|
+
# port: 3306
|
48
|
+
# env:
|
49
|
+
# clear:
|
50
|
+
# MYSQL_ROOT_HOST: '%'
|
51
|
+
# secret:
|
52
|
+
# - MYSQL_ROOT_PASSWORD
|
53
|
+
# files:
|
54
|
+
# - config/mysql/production.cnf:/etc/mysql/my.cnf
|
55
|
+
# - db/production.sql.erb:/docker-entrypoint-initdb.d/setup.sql
|
56
|
+
# directories:
|
57
|
+
# - data:/var/lib/mysql
|
58
|
+
# redis:
|
59
|
+
# image: redis:7.0
|
60
|
+
# host: 192.168.0.2
|
61
|
+
# port: 6379
|
62
|
+
# directories:
|
63
|
+
# - data:/data
|
64
|
+
|
65
|
+
# Configure custom arguments for Traefik
|
66
|
+
# traefik:
|
67
|
+
# args:
|
68
|
+
# accesslog: true
|
69
|
+
# accesslog.format: json
|
70
|
+
|
71
|
+
# Configure a custom healthcheck (default is /up on port 3000)
|
72
|
+
# healthcheck:
|
73
|
+
# path: /healthz
|
74
|
+
# port: 4000
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
# A sample post-deploy hook
|
4
|
+
#
|
5
|
+
# These environment variables are available:
|
6
|
+
# KAMAL_RECORDED_AT
|
7
|
+
# KAMAL_PERFORMER
|
8
|
+
# KAMAL_VERSION
|
9
|
+
# KAMAL_HOSTS
|
10
|
+
# KAMAL_ROLE (if set)
|
11
|
+
# KAMAL_DESTINATION (if set)
|
12
|
+
# KAMAL_RUNTIME
|
13
|
+
|
14
|
+
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
# A sample pre-build hook
|
4
|
+
#
|
5
|
+
# Checks:
|
6
|
+
# 1. We have a clean checkout
|
7
|
+
# 2. A remote is configured
|
8
|
+
# 3. The branch has been pushed to the remote
|
9
|
+
# 4. The version we are deploying matches the remote
|
10
|
+
#
|
11
|
+
# These environment variables are available:
|
12
|
+
# KAMAL_RECORDED_AT
|
13
|
+
# KAMAL_PERFORMER
|
14
|
+
# KAMAL_VERSION
|
15
|
+
# KAMAL_HOSTS
|
16
|
+
# KAMAL_ROLE (if set)
|
17
|
+
# KAMAL_DESTINATION (if set)
|
18
|
+
|
19
|
+
if [ -n "$(git status --porcelain)" ]; then
|
20
|
+
echo "Git checkout is not clean, aborting..." >&2
|
21
|
+
git status --porcelain >&2
|
22
|
+
exit 1
|
23
|
+
fi
|
24
|
+
|
25
|
+
first_remote=$(git remote)
|
26
|
+
|
27
|
+
if [ -z "$first_remote" ]; then
|
28
|
+
echo "No git remote set, aborting..." >&2
|
29
|
+
exit 1
|
30
|
+
fi
|
31
|
+
|
32
|
+
current_branch=$(git branch --show-current)
|
33
|
+
|
34
|
+
if [ -z "$current_branch" ]; then
|
35
|
+
echo "No git remote set, aborting..." >&2
|
36
|
+
exit 1
|
37
|
+
fi
|
38
|
+
|
39
|
+
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
|
40
|
+
|
41
|
+
if [ -z "$remote_head" ]; then
|
42
|
+
echo "Branch not pushed to remote, aborting..." >&2
|
43
|
+
exit 1
|
44
|
+
fi
|
45
|
+
|
46
|
+
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
|
47
|
+
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
|
48
|
+
exit 1
|
49
|
+
fi
|
50
|
+
|
51
|
+
exit 0
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# A sample pre-connect check
|
4
|
+
#
|
5
|
+
# Warms DNS before connecting to hosts in parallel
|
6
|
+
#
|
7
|
+
# These environment variables are available:
|
8
|
+
# KAMAL_RECORDED_AT
|
9
|
+
# KAMAL_PERFORMER
|
10
|
+
# KAMAL_VERSION
|
11
|
+
# KAMAL_HOSTS
|
12
|
+
# KAMAL_ROLE (if set)
|
13
|
+
# KAMAL_DESTINATION (if set)
|
14
|
+
# KAMAL_RUNTIME
|
15
|
+
|
16
|
+
hosts = ENV["KAMAL_HOSTS"].split(",")
|
17
|
+
results = nil
|
18
|
+
max = 3
|
19
|
+
|
20
|
+
elapsed = Benchmark.realtime do
|
21
|
+
results = hosts.map do |host|
|
22
|
+
Thread.new do
|
23
|
+
tries = 1
|
24
|
+
|
25
|
+
begin
|
26
|
+
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
|
27
|
+
rescue SocketError
|
28
|
+
if tries < max
|
29
|
+
puts "Retrying DNS warmup: #{host}"
|
30
|
+
tries += 1
|
31
|
+
sleep rand
|
32
|
+
retry
|
33
|
+
else
|
34
|
+
puts "DNS warmup failed: #{host}"
|
35
|
+
host
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
tries
|
40
|
+
end
|
41
|
+
end.map(&:value)
|
42
|
+
end
|
43
|
+
|
44
|
+
retries = results.sum - hosts.size
|
45
|
+
nopes = results.count { |r| r == max }
|
46
|
+
|
47
|
+
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
|
@@ -0,0 +1,109 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# A sample pre-deploy hook
|
4
|
+
#
|
5
|
+
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
|
6
|
+
#
|
7
|
+
# Fails unless the combined status is "success"
|
8
|
+
#
|
9
|
+
# These environment variables are available:
|
10
|
+
# KAMAL_RECORDED_AT
|
11
|
+
# KAMAL_PERFORMER
|
12
|
+
# KAMAL_VERSION
|
13
|
+
# KAMAL_HOSTS
|
14
|
+
# KAMAL_COMMAND
|
15
|
+
# KAMAL_SUBCOMMAND
|
16
|
+
# KAMAL_ROLE (if set)
|
17
|
+
# KAMAL_DESTINATION (if set)
|
18
|
+
|
19
|
+
# Only check the build status for production deployments
|
20
|
+
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
|
21
|
+
exit 0
|
22
|
+
end
|
23
|
+
|
24
|
+
require "bundler/inline"
|
25
|
+
|
26
|
+
# true = install gems so this is fast on repeat invocations
|
27
|
+
gemfile(true, quiet: true) do
|
28
|
+
source "https://rubygems.org"
|
29
|
+
|
30
|
+
gem "octokit"
|
31
|
+
gem "faraday-retry"
|
32
|
+
end
|
33
|
+
|
34
|
+
MAX_ATTEMPTS = 72
|
35
|
+
ATTEMPTS_GAP = 10
|
36
|
+
|
37
|
+
def exit_with_error(message)
|
38
|
+
$stderr.puts message
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
|
42
|
+
class GithubStatusChecks
|
43
|
+
attr_reader :remote_url, :git_sha, :github_client, :combined_status
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
|
47
|
+
@git_sha = `git rev-parse HEAD`.strip
|
48
|
+
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
49
|
+
refresh!
|
50
|
+
end
|
51
|
+
|
52
|
+
def refresh!
|
53
|
+
@combined_status = github_client.combined_status(remote_url, git_sha)
|
54
|
+
end
|
55
|
+
|
56
|
+
def state
|
57
|
+
combined_status[:state]
|
58
|
+
end
|
59
|
+
|
60
|
+
def first_status_url
|
61
|
+
first_status = combined_status[:statuses].find { |status| status[:state] == state }
|
62
|
+
first_status && first_status[:target_url]
|
63
|
+
end
|
64
|
+
|
65
|
+
def complete_count
|
66
|
+
combined_status[:statuses].count { |status| status[:state] != "pending"}
|
67
|
+
end
|
68
|
+
|
69
|
+
def total_count
|
70
|
+
combined_status[:statuses].count
|
71
|
+
end
|
72
|
+
|
73
|
+
def current_status
|
74
|
+
if total_count > 0
|
75
|
+
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
|
76
|
+
else
|
77
|
+
"Build not started..."
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
$stdout.sync = true
|
84
|
+
|
85
|
+
puts "Checking build status..."
|
86
|
+
attempts = 0
|
87
|
+
checks = GithubStatusChecks.new
|
88
|
+
|
89
|
+
begin
|
90
|
+
loop do
|
91
|
+
case checks.state
|
92
|
+
when "success"
|
93
|
+
puts "Checks passed, see #{checks.first_status_url}"
|
94
|
+
exit 0
|
95
|
+
when "failure"
|
96
|
+
exit_with_error "Checks failed, see #{checks.first_status_url}"
|
97
|
+
when "pending"
|
98
|
+
attempts += 1
|
99
|
+
end
|
100
|
+
|
101
|
+
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
|
102
|
+
|
103
|
+
puts checks.current_status
|
104
|
+
sleep(ATTEMPTS_GAP)
|
105
|
+
checks.refresh!
|
106
|
+
end
|
107
|
+
rescue Octokit::NotFound
|
108
|
+
exit_with_error "Build status could not be found"
|
109
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
class Kamal::Cli::Traefik < Kamal::Cli::Base
|
2
|
+
desc "boot", "Boot Traefik on servers"
|
3
|
+
def boot
|
4
|
+
mutating do
|
5
|
+
on(KAMAL.traefik_hosts) do
|
6
|
+
execute *KAMAL.registry.login
|
7
|
+
execute *KAMAL.traefik.start_or_run
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
13
|
+
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
14
|
+
def reboot
|
15
|
+
mutating do
|
16
|
+
on(KAMAL.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do
|
17
|
+
execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
|
18
|
+
execute *KAMAL.registry.login
|
19
|
+
execute *KAMAL.traefik.stop
|
20
|
+
execute *KAMAL.traefik.remove_container
|
21
|
+
execute *KAMAL.traefik.run
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "start", "Start existing Traefik container on servers"
|
27
|
+
def start
|
28
|
+
mutating do
|
29
|
+
on(KAMAL.traefik_hosts) do
|
30
|
+
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
|
31
|
+
execute *KAMAL.traefik.start
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "stop", "Stop existing Traefik container on servers"
|
37
|
+
def stop
|
38
|
+
mutating do
|
39
|
+
on(KAMAL.traefik_hosts) do
|
40
|
+
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
|
41
|
+
execute *KAMAL.traefik.stop
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "restart", "Restart existing Traefik container on servers"
|
47
|
+
def restart
|
48
|
+
mutating do
|
49
|
+
stop
|
50
|
+
start
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "details", "Show details about Traefik container from servers"
|
55
|
+
def details
|
56
|
+
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "logs", "Show log lines from Traefik on servers"
|
60
|
+
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
61
|
+
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
62
|
+
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
63
|
+
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
64
|
+
def logs
|
65
|
+
grep = options[:grep]
|
66
|
+
|
67
|
+
if options[:follow]
|
68
|
+
run_locally do
|
69
|
+
info "Following logs on #{KAMAL.primary_host}..."
|
70
|
+
info KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
|
71
|
+
exec KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
since = options[:since]
|
75
|
+
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
76
|
+
|
77
|
+
on(KAMAL.traefik_hosts) do |host|
|
78
|
+
puts_by_host host, capture(*KAMAL.traefik.logs(since: since, lines: lines, grep: grep)), type: "Traefik"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "remove", "Remove Traefik container and image from servers"
|
84
|
+
def remove
|
85
|
+
mutating do
|
86
|
+
stop
|
87
|
+
remove_container
|
88
|
+
remove_image
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
desc "remove_container", "Remove Traefik container from servers", hide: true
|
93
|
+
def remove_container
|
94
|
+
mutating do
|
95
|
+
on(KAMAL.traefik_hosts) do
|
96
|
+
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
|
97
|
+
execute *KAMAL.traefik.remove_container
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
desc "remove_image", "Remove Traefik image from servers", hide: true
|
103
|
+
def remove_image
|
104
|
+
mutating do
|
105
|
+
on(KAMAL.traefik_hosts) do
|
106
|
+
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
|
107
|
+
execute *KAMAL.traefik.remove_image
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/kamal/cli.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require "active_support/core_ext/enumerable"
|
2
|
+
require "active_support/core_ext/module/delegation"
|
3
|
+
|
4
|
+
class Kamal::Commander
|
5
|
+
attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.verbosity = :info
|
9
|
+
self.holding_lock = false
|
10
|
+
self.hold_lock_on_error = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def config
|
14
|
+
@config ||= Kamal::Configuration.create_from(**@config_kwargs).tap do |config|
|
15
|
+
@config_kwargs = nil
|
16
|
+
configure_sshkit_with(config)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(**kwargs)
|
21
|
+
@config, @config_kwargs = nil, kwargs
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :specific_roles, :specific_hosts
|
25
|
+
|
26
|
+
def specific_primary!
|
27
|
+
self.specific_hosts = [ config.primary_web_host ]
|
28
|
+
end
|
29
|
+
|
30
|
+
def specific_roles=(role_names)
|
31
|
+
@specific_roles = config.roles.select { |r| role_names.include?(r.name) } if role_names.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
def specific_hosts=(hosts)
|
35
|
+
@specific_hosts = config.all_hosts & hosts if hosts.present?
|
36
|
+
end
|
37
|
+
|
38
|
+
def primary_host
|
39
|
+
specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
|
40
|
+
end
|
41
|
+
|
42
|
+
def roles
|
43
|
+
(specific_roles || config.roles).select do |role|
|
44
|
+
((specific_hosts || config.all_hosts) & role.hosts).any?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def hosts
|
49
|
+
(specific_hosts || config.all_hosts).select do |host|
|
50
|
+
(specific_roles || config.roles).flat_map(&:hosts).include?(host)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def boot_strategy
|
55
|
+
if config.boot.limit.present?
|
56
|
+
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
57
|
+
else
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def roles_on(host)
|
63
|
+
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def traefik_hosts
|
67
|
+
specific_hosts || config.traefik_hosts
|
68
|
+
end
|
69
|
+
|
70
|
+
def accessory_hosts
|
71
|
+
specific_hosts || config.accessories.flat_map(&:hosts)
|
72
|
+
end
|
73
|
+
|
74
|
+
def accessory_names
|
75
|
+
config.accessories&.collect(&:name) || []
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def app(role: nil)
|
80
|
+
Kamal::Commands::App.new(config, role: role)
|
81
|
+
end
|
82
|
+
|
83
|
+
def accessory(name)
|
84
|
+
Kamal::Commands::Accessory.new(config, name: name)
|
85
|
+
end
|
86
|
+
|
87
|
+
def auditor(**details)
|
88
|
+
Kamal::Commands::Auditor.new(config, **details)
|
89
|
+
end
|
90
|
+
|
91
|
+
def builder
|
92
|
+
@builder ||= Kamal::Commands::Builder.new(config)
|
93
|
+
end
|
94
|
+
|
95
|
+
def docker
|
96
|
+
@docker ||= Kamal::Commands::Docker.new(config)
|
97
|
+
end
|
98
|
+
|
99
|
+
def healthcheck
|
100
|
+
@healthcheck ||= Kamal::Commands::Healthcheck.new(config)
|
101
|
+
end
|
102
|
+
|
103
|
+
def hook
|
104
|
+
@hook ||= Kamal::Commands::Hook.new(config)
|
105
|
+
end
|
106
|
+
|
107
|
+
def lock
|
108
|
+
@lock ||= Kamal::Commands::Lock.new(config)
|
109
|
+
end
|
110
|
+
|
111
|
+
def prune
|
112
|
+
@prune ||= Kamal::Commands::Prune.new(config)
|
113
|
+
end
|
114
|
+
|
115
|
+
def registry
|
116
|
+
@registry ||= Kamal::Commands::Registry.new(config)
|
117
|
+
end
|
118
|
+
|
119
|
+
def traefik
|
120
|
+
@traefik ||= Kamal::Commands::Traefik.new(config)
|
121
|
+
end
|
122
|
+
|
123
|
+
def with_verbosity(level)
|
124
|
+
old_level = self.verbosity
|
125
|
+
|
126
|
+
self.verbosity = level
|
127
|
+
SSHKit.config.output_verbosity = level
|
128
|
+
|
129
|
+
yield
|
130
|
+
ensure
|
131
|
+
self.verbosity = old_level
|
132
|
+
SSHKit.config.output_verbosity = old_level
|
133
|
+
end
|
134
|
+
|
135
|
+
def holding_lock?
|
136
|
+
self.holding_lock
|
137
|
+
end
|
138
|
+
|
139
|
+
def hold_lock_on_error?
|
140
|
+
self.hold_lock_on_error
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
# Lazy setup of SSHKit
|
145
|
+
def configure_sshkit_with(config)
|
146
|
+
SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
|
147
|
+
SSHKit::Backend::Netssh.configure do |sshkit|
|
148
|
+
sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
|
149
|
+
sshkit.ssh_options = config.ssh.options
|
150
|
+
end
|
151
|
+
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
|
152
|
+
SSHKit.config.output_verbosity = verbosity
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
2
|
+
attr_reader :accessory_config
|
3
|
+
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
4
|
+
:publish_args, :env_args, :volume_args, :label_args, :option_args, to: :accessory_config
|
5
|
+
|
6
|
+
def initialize(config, name:)
|
7
|
+
super(config)
|
8
|
+
@accessory_config = config.accessory(name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
docker :run,
|
13
|
+
"--name", service_name,
|
14
|
+
"--detach",
|
15
|
+
"--restart", "unless-stopped",
|
16
|
+
*config.logging_args,
|
17
|
+
*publish_args,
|
18
|
+
*env_args,
|
19
|
+
*volume_args,
|
20
|
+
*label_args,
|
21
|
+
*option_args,
|
22
|
+
image,
|
23
|
+
cmd
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
docker :container, :start, service_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
docker :container, :stop, service_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def info
|
35
|
+
docker :ps, *service_filter
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def logs(since: nil, lines: nil, grep: nil)
|
40
|
+
pipe \
|
41
|
+
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
|
42
|
+
("grep '#{grep}'" if grep)
|
43
|
+
end
|
44
|
+
|
45
|
+
def follow_logs(grep: nil)
|
46
|
+
run_over_ssh \
|
47
|
+
pipe \
|
48
|
+
docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
49
|
+
(%(grep "#{grep}") if grep)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def execute_in_existing_container(*command, interactive: false)
|
54
|
+
docker :exec,
|
55
|
+
("-it" if interactive),
|
56
|
+
service_name,
|
57
|
+
*command
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute_in_new_container(*command, interactive: false)
|
61
|
+
docker :run,
|
62
|
+
("-it" if interactive),
|
63
|
+
"--rm",
|
64
|
+
*env_args,
|
65
|
+
*volume_args,
|
66
|
+
image,
|
67
|
+
*command
|
68
|
+
end
|
69
|
+
|
70
|
+
def execute_in_existing_container_over_ssh(*command)
|
71
|
+
run_over_ssh execute_in_existing_container(*command, interactive: true)
|
72
|
+
end
|
73
|
+
|
74
|
+
def execute_in_new_container_over_ssh(*command)
|
75
|
+
run_over_ssh execute_in_new_container(*command, interactive: true)
|
76
|
+
end
|
77
|
+
|
78
|
+
def run_over_ssh(command)
|
79
|
+
super command, host: hosts.first
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def ensure_local_file_present(local_file)
|
84
|
+
if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
|
85
|
+
raise "Missing file: #{local_file}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def make_directory_for(remote_file)
|
90
|
+
make_directory Pathname.new(remote_file).dirname.to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
def make_directory(path)
|
94
|
+
[ :mkdir, "-p", path ]
|
95
|
+
end
|
96
|
+
|
97
|
+
def remove_service_directory
|
98
|
+
[ :rm, "-rf", service_name ]
|
99
|
+
end
|
100
|
+
|
101
|
+
def remove_container
|
102
|
+
docker :container, :prune, "--force", *service_filter
|
103
|
+
end
|
104
|
+
|
105
|
+
def remove_image
|
106
|
+
docker :image, :rm, "--force", image
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
def service_filter
|
111
|
+
[ "--filter", "label=service=#{service_name}" ]
|
112
|
+
end
|
113
|
+
end
|