shippy 0.2.5 → 0.2.6
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 +4 -4
- data/lib/shippy/builder.rb +32 -0
- data/lib/shippy/cli/app.rb +27 -188
- data/lib/shippy/cli/init.rb +8 -5
- data/lib/shippy/cli/prune.rb +15 -10
- data/lib/shippy/cli/templates/config/shippy.yml +1 -0
- data/lib/shippy/commander.rb +11 -14
- data/lib/shippy/config.rb +20 -6
- data/lib/shippy/deployer.rb +60 -0
- data/lib/shippy/docker/client.rb +95 -0
- data/lib/shippy/docker/system.rb +23 -0
- data/lib/shippy/maintainer.rb +33 -0
- data/lib/shippy/remote/app.rb +75 -0
- data/lib/shippy/ssh_stream_handler.rb +12 -0
- data/lib/shippy/version.rb +1 -1
- data/lib/shippy.rb +7 -0
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9ad6a9811193f1cba32cbbbb3caaadbe7baabd6cdeedd7b9d3462af3a311e41
|
|
4
|
+
data.tar.gz: 68c148787b05aaa7bd3cea51bad53ab73638c592d0dd1967329fcda427d64466
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 926e9d0c76ff27f776ff16917b4c6142c3446961f2f4b343e15ad88096a8bcdbd1e53de6e699686f058c6c51b506da9ad4608c1fc143af282bb68d981c90c1c6
|
|
7
|
+
data.tar.gz: dbf4e8d28ac653c354effda9ec3ec1ff901c4d3c1554e41db644d92b226c98c840fbf1f963b876f182f91861d2ff248f7854560374d28a6af3bcff847c6ef9e4
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "minitar"
|
|
2
|
+
require "zlib"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Shippy
|
|
6
|
+
class Builder
|
|
7
|
+
attr_reader :archive_path
|
|
8
|
+
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
@archive_path = "/tmp/v#{Time.now.to_i}.tar.gz"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile
|
|
15
|
+
Shippy::Compiler.new(@app, config: SHIPPY.config).compile
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def archive
|
|
19
|
+
FileUtils.cd("builds/apps") do
|
|
20
|
+
File.open(@archive_path, "wb") do |file|
|
|
21
|
+
zlib = Zlib::GzipWriter.new(file)
|
|
22
|
+
Minitar.pack(@app.name, zlib)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
@archive_path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cleanup
|
|
29
|
+
FileUtils.rm_f(@archive_path)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/shippy/cli/app.rb
CHANGED
|
@@ -3,223 +3,62 @@ class Shippy::Cli::App < Shippy::Cli::Base
|
|
|
3
3
|
|
|
4
4
|
def initialize(...)
|
|
5
5
|
super
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
desc "compile", "compile application"
|
|
10
|
-
def compile
|
|
11
|
-
say "compiling #{app_name} on #{SHIPPY.host}...", :magenta
|
|
12
|
-
|
|
13
|
-
SHIPPY.compile
|
|
6
|
+
@remote_app = Shippy::Remote::App.new(SHIPPY.host, app_name)
|
|
7
|
+
@docker = Shippy::Docker::Client.new(SHIPPY.host)
|
|
14
8
|
end
|
|
15
9
|
|
|
16
10
|
desc "deploy", "Deploy application"
|
|
17
11
|
def deploy
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
upload
|
|
24
|
-
create_network
|
|
25
|
-
refresh
|
|
26
|
-
stop_old
|
|
27
|
-
pre_start_hooks
|
|
28
|
-
start
|
|
29
|
-
cleanup
|
|
30
|
-
status
|
|
12
|
+
Shippy::Deployer.new(
|
|
13
|
+
app_name: app_name,
|
|
14
|
+
host: SHIPPY.host,
|
|
15
|
+
ui: self
|
|
16
|
+
).deploy
|
|
31
17
|
end
|
|
32
18
|
|
|
33
19
|
desc "refresh", "Pull new images"
|
|
34
20
|
def refresh
|
|
35
|
-
say "Pulling images
|
|
36
|
-
|
|
37
|
-
within_app do
|
|
38
|
-
execute :docker, "compose", "pull"
|
|
39
|
-
end
|
|
21
|
+
say "Pulling images...", :magenta
|
|
22
|
+
@docker.compose_pull(@remote_app.current_path, stream: true)
|
|
40
23
|
end
|
|
41
24
|
|
|
42
25
|
desc "start", "Start application"
|
|
43
26
|
def start
|
|
44
|
-
say "Starting
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
execute :docker, "compose", "up", "-d", SHIPPY.app&.deploy_flags
|
|
48
|
-
end
|
|
27
|
+
say "Starting...", :magenta
|
|
28
|
+
app_config = Shippy::Repo.load_app(app_name)
|
|
29
|
+
@docker.compose_up(@remote_app.current_path, flags: app_config&.deploy_flags)
|
|
49
30
|
end
|
|
50
31
|
|
|
51
32
|
desc "restart", "Restart application"
|
|
52
|
-
option :service, aliases: "-S", type: :string
|
|
33
|
+
option :service, aliases: "-S", type: :string
|
|
53
34
|
def restart
|
|
54
|
-
say "Restarting
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
within_app do
|
|
58
|
-
execute :docker, "compose", "restart", *restart_options
|
|
59
|
-
end
|
|
35
|
+
say "Restarting...", :magenta
|
|
36
|
+
services = options[:service] ? [options[:service]] : []
|
|
37
|
+
@docker.compose_restart(@remote_app.current_path, services)
|
|
60
38
|
end
|
|
61
39
|
|
|
62
40
|
desc "stop", "Stop application"
|
|
63
41
|
def stop
|
|
64
|
-
say "Stopping
|
|
65
|
-
|
|
66
|
-
within_app do
|
|
67
|
-
execute :docker, "compose", "down", "--remove-orphans"
|
|
68
|
-
end
|
|
42
|
+
say "Stopping...", :magenta
|
|
43
|
+
@docker.compose_down(@remote_app.current_path)
|
|
69
44
|
end
|
|
70
45
|
|
|
71
46
|
desc "logs", "Show app logs"
|
|
47
|
+
option :follow, aliases: "-f", type: :boolean
|
|
72
48
|
def logs
|
|
73
|
-
|
|
74
|
-
puts capture(:docker, "compose", "logs", verbosity: Logger::INFO)
|
|
75
|
-
end
|
|
49
|
+
@docker.logs(@remote_app.current_path, follow: options[:follow])
|
|
76
50
|
end
|
|
77
51
|
|
|
78
52
|
desc "status", "Show app status"
|
|
79
53
|
def status
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
within_app do
|
|
83
|
-
puts capture(:docker, "compose", "ps", "-a")
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
private
|
|
88
|
-
|
|
89
|
-
def load_app
|
|
90
|
-
with_app do
|
|
91
|
-
SHIPPY.app = Shippy::Repo.load_app(app_name)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def archive
|
|
96
|
-
FileUtils.cd("builds/apps") do
|
|
97
|
-
Minitar.pack(app_name, Zlib::GzipWriter.new(File.open(archive_path, "wb")))
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def prepare_directory
|
|
102
|
-
on(SHIPPY.host) do
|
|
103
|
-
execute :mkdir, "-p", SHIPPY.current_app_path
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def backup
|
|
108
|
-
backups_path = self.backups_path
|
|
109
|
-
|
|
110
|
-
on(SHIPPY.host) do
|
|
111
|
-
if test("[ -d #{SHIPPY.current_app_path} ]")
|
|
112
|
-
execute :mkdir, "-p", backups_path
|
|
113
|
-
execute :mv, SHIPPY.current_app_path, backups_path
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def stop_old
|
|
119
|
-
backup_path = backups_path.join(app_name)
|
|
120
|
-
|
|
121
|
-
on(SHIPPY.host) do
|
|
122
|
-
if test("[ -f #{backup_path.join("docker-compose.yml")} ]")
|
|
123
|
-
within backup_path do
|
|
124
|
-
execute :docker, "compose", "down", "--remove-orphans"
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def upload
|
|
131
|
-
source_path = SHIPPY.current_app_path.join("..").expand_path
|
|
132
|
-
archive_path = self.archive_path
|
|
133
|
-
prepare_directory
|
|
134
|
-
|
|
135
|
-
on(SHIPPY.host) do
|
|
136
|
-
tmp = capture "mktemp"
|
|
137
|
-
upload! archive_path, tmp
|
|
138
|
-
|
|
139
|
-
execute :tar, "-xzpf", tmp, "-C", source_path
|
|
140
|
-
execute :rm, tmp
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def cleanup
|
|
145
|
-
remove_archive
|
|
146
|
-
remove_old_releases
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def remove_archive
|
|
150
|
-
archive_path = self.archive_path
|
|
151
|
-
|
|
152
|
-
run_locally do
|
|
153
|
-
execute :rm, archive_path
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def remove_old_releases
|
|
158
|
-
app_backups_paths = backups_path.join("..").expand_path
|
|
159
|
-
|
|
160
|
-
on(SHIPPY.host) do
|
|
161
|
-
if test("[ -d #{app_backups_paths} ]")
|
|
162
|
-
releases = capture(:ls, "-x", app_backups_paths).split
|
|
163
|
-
directories = (releases - releases.last(SHIPPY.config.keep_releases)).map do |release|
|
|
164
|
-
app_backups_paths.join(release).to_s
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
if directories.any?
|
|
168
|
-
directories.each_slice(100) do |directories_batch|
|
|
169
|
-
execute :rm, "-rf", *directories_batch
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def pre_start_hooks
|
|
177
|
-
say "Running #{app_name} hooks on #{SHIPPY.host}...", :magenta
|
|
178
|
-
|
|
179
|
-
within_app do
|
|
180
|
-
SHIPPY.app.each_hook do |service, options, command|
|
|
181
|
-
execute :docker, "compose", "run", "--rm", options, service, command
|
|
182
|
-
end
|
|
183
|
-
end
|
|
54
|
+
@docker.status(@remote_app.current_path)
|
|
184
55
|
end
|
|
185
56
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def archive_path
|
|
197
|
-
@archive_path ||= "/tmp/v#{Time.now.to_i}.tar.gz"
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def backups_path
|
|
201
|
-
@backups_path ||= SHIPPY.deploy_path.join("backups", app_name, Time.now.to_i.to_s)
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def with_app
|
|
205
|
-
path = Pathname.new("apps").join(app_name)
|
|
206
|
-
|
|
207
|
-
if path.exist?
|
|
208
|
-
yield
|
|
209
|
-
else
|
|
210
|
-
error_on_missing_app
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def error_on_missing_app
|
|
215
|
-
raise "No app by the name of '#{app_name}'"
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def within_app(&block)
|
|
219
|
-
on(SHIPPY.host) do
|
|
220
|
-
within(SHIPPY.current_app_path) do
|
|
221
|
-
instance_eval(&block)
|
|
222
|
-
end
|
|
223
|
-
end
|
|
57
|
+
desc "compile", "Compile application"
|
|
58
|
+
def compile
|
|
59
|
+
say "Compiling #{app_name}...", :magenta
|
|
60
|
+
# Load app config and pass to builder
|
|
61
|
+
app_config = Shippy::Repo.load_app(app_name)
|
|
62
|
+
Shippy::Builder.new(app_config).compile
|
|
224
63
|
end
|
|
225
64
|
end
|
data/lib/shippy/cli/init.rb
CHANGED
|
@@ -13,10 +13,6 @@ class Shippy::Cli::Init < Thor::Group
|
|
|
13
13
|
copy_file "templates/config/shippy.yml", "config/shippy.yml"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def create_secrets_file
|
|
17
|
-
copy_file "templates/config/secrets.yml", "config/secrets.yml"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
16
|
def init_gemfile
|
|
21
17
|
return if ::File.exist?("Gemfile")
|
|
22
18
|
|
|
@@ -24,7 +20,9 @@ class Shippy::Cli::Init < Thor::Group
|
|
|
24
20
|
end
|
|
25
21
|
|
|
26
22
|
def add_shippy_to_gemfile
|
|
27
|
-
|
|
23
|
+
unless File.read("Gemfile").include?("shippy")
|
|
24
|
+
insert_into_file "Gemfile", "gem 'shippy', '~> #{Shippy::VERSION}'"
|
|
25
|
+
end
|
|
28
26
|
|
|
29
27
|
run "bundle install", abort_on_failure: false
|
|
30
28
|
end
|
|
@@ -46,5 +44,10 @@ class Shippy::Cli::Init < Thor::Group
|
|
|
46
44
|
insert_into_file ".gitignore", "/.bundle/\n"
|
|
47
45
|
insert_into_file ".gitignore", "/builds/\n"
|
|
48
46
|
insert_into_file ".gitignore", "/config/secrets.yml\n"
|
|
47
|
+
insert_into_file ".gitignore", "/config/master.key\n"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generate_secret!
|
|
51
|
+
Shippy::SecretsManager.new.ensure_key_exists!
|
|
49
52
|
end
|
|
50
53
|
end
|
data/lib/shippy/cli/prune.rb
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
class Shippy::Cli::Prune < Shippy::Cli::Base
|
|
2
2
|
desc "all", "Prune unused images and stopped containers"
|
|
3
3
|
def all
|
|
4
|
-
|
|
5
|
-
images
|
|
4
|
+
maintainer.clean_all
|
|
6
5
|
end
|
|
7
6
|
|
|
8
|
-
desc "images", "Prune unused images
|
|
7
|
+
desc "images", "Prune unused images"
|
|
8
|
+
option :hours, type: :numeric, desc: "Override default retention hours"
|
|
9
9
|
def images
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
end
|
|
10
|
+
hours = options[:hours] || Shippy::Maintainer::IMAGE_RETENTION_HOURS
|
|
11
|
+
maintainer.clean_images(hours: hours)
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
desc "containers", "Prune stopped containers
|
|
14
|
+
desc "containers", "Prune stopped containers"
|
|
15
|
+
option :hours, type: :numeric, desc: "Override default retention hours"
|
|
16
16
|
def containers
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
hours = options[:hours] || Shippy::Maintainer::CONTAINER_RETENTION_HOURS
|
|
18
|
+
maintainer.clean_containers(hours: hours)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def maintainer
|
|
24
|
+
@maintainer ||= Shippy::Maintainer.new(SHIPPY.host, ui: self)
|
|
20
25
|
end
|
|
21
26
|
end
|
data/lib/shippy/commander.rb
CHANGED
|
@@ -1,42 +1,39 @@
|
|
|
1
1
|
module Shippy
|
|
2
2
|
class Commander
|
|
3
|
-
attr_accessor :config_file, :verbosity
|
|
3
|
+
attr_accessor :config_file, :verbosity
|
|
4
|
+
|
|
4
5
|
delegate :host, :deploy_path, to: :config
|
|
5
6
|
|
|
6
7
|
def initialize(config_file: nil, verbosity: :info)
|
|
7
|
-
@config_file
|
|
8
|
+
@config_file = config_file
|
|
9
|
+
@verbosity = verbosity
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def config
|
|
11
13
|
@config ||= Shippy::Config
|
|
12
14
|
.new(config_file)
|
|
13
|
-
.tap { |
|
|
15
|
+
.tap { |c| configure_sshkit_with(c) }
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def with_verbosity(level)
|
|
17
19
|
old_level = verbosity
|
|
18
|
-
|
|
19
20
|
self.verbosity = level
|
|
20
21
|
SSHKit.config.output_verbosity = level
|
|
21
|
-
|
|
22
22
|
yield
|
|
23
23
|
ensure
|
|
24
24
|
self.verbosity = old_level
|
|
25
25
|
SSHKit.config.output_verbosity = old_level
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def compile
|
|
29
|
-
Shippy::Compiler.new(app, config: config).compile
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def current_app_path
|
|
33
|
-
deploy_path.join("apps", app.name)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
28
|
private
|
|
37
29
|
|
|
38
30
|
def configure_sshkit_with(config)
|
|
39
|
-
SSHKit::Backend::Netssh.configure
|
|
31
|
+
SSHKit::Backend::Netssh.configure do |ssh|
|
|
32
|
+
ssh.ssh_options = config.ssh_options
|
|
33
|
+
ssh.pty = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Map commands if they aren't standard on the remote
|
|
40
37
|
SSHKit.config.command_map[:docker] = "docker"
|
|
41
38
|
SSHKit.config.command_map[:docker_compose] = "docker-compose"
|
|
42
39
|
SSHKit.config.output_verbosity = verbosity
|
data/lib/shippy/config.rb
CHANGED
|
@@ -12,15 +12,29 @@ module Shippy
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def ssh_options
|
|
15
|
-
|
|
15
|
+
options = {
|
|
16
|
+
user: ssh_user,
|
|
17
|
+
port: ssh_port,
|
|
18
|
+
keys: ssh_keys,
|
|
19
|
+
auth_methods: ["publickey"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
options.compact
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def ssh_user
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
data.dig(:ssh, :user) || "root"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ssh_port
|
|
30
|
+
data.dig(:ssh, :port)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ssh_keys
|
|
34
|
+
keys = data.dig(:ssh, :keys)
|
|
35
|
+
return if keys.nil?
|
|
36
|
+
|
|
37
|
+
Array(keys).map { |k| File.expand_path(k) }
|
|
24
38
|
end
|
|
25
39
|
|
|
26
40
|
def deploy_path
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Shippy
|
|
2
|
+
class Deployer
|
|
3
|
+
def initialize(app_name:, host:, ui:)
|
|
4
|
+
@app_name = app_name
|
|
5
|
+
@ui = ui
|
|
6
|
+
@app_config = Shippy::Repo.load_app(app_name)
|
|
7
|
+
@builder = Shippy::Builder.new(@app_config)
|
|
8
|
+
@remote_app = Shippy::Remote::App.new(host, app_name)
|
|
9
|
+
@docker = Shippy::Docker::Client.new(host)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def deploy
|
|
13
|
+
@ui.say "Deploying #{@app_name}...", :magenta
|
|
14
|
+
|
|
15
|
+
@builder.compile
|
|
16
|
+
archive = @builder.archive
|
|
17
|
+
|
|
18
|
+
@ui.say "Backing up current version...", :blue
|
|
19
|
+
backup_path = @remote_app.backup_current_release
|
|
20
|
+
|
|
21
|
+
@ui.say "Uploading new version...", :blue
|
|
22
|
+
@remote_app.prepare_directories
|
|
23
|
+
@remote_app.upload_release(archive)
|
|
24
|
+
|
|
25
|
+
@docker.ensure_network("lan_access")
|
|
26
|
+
|
|
27
|
+
@ui.say "Pulling images...", :blue
|
|
28
|
+
@docker.compose_pull(@remote_app.current_path, stream: true)
|
|
29
|
+
|
|
30
|
+
if backup_path
|
|
31
|
+
@ui.say "Stopping old version...", :yellow
|
|
32
|
+
@docker.compose_down(backup_path)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
run_hooks
|
|
36
|
+
|
|
37
|
+
@ui.say "Starting application...", :green
|
|
38
|
+
@docker.compose_up(@remote_app.current_path, flags: @app_config&.deploy_flags)
|
|
39
|
+
|
|
40
|
+
@builder.cleanup
|
|
41
|
+
@remote_app.prune_old_releases(keep: SHIPPY.config.keep_releases)
|
|
42
|
+
|
|
43
|
+
@docker.status(@remote_app.current_path)
|
|
44
|
+
rescue => e
|
|
45
|
+
@ui.error "Deployment failed: #{e.message}"
|
|
46
|
+
raise e
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def run_hooks
|
|
52
|
+
return unless @app_config
|
|
53
|
+
|
|
54
|
+
@app_config.each_hook do |service, options, command|
|
|
55
|
+
@ui.say "Running hook: #{command}", :cyan
|
|
56
|
+
@docker.run_hook(@remote_app.current_path, service, options, command)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require "sshkit"
|
|
2
|
+
require "sshkit/dsl"
|
|
3
|
+
|
|
4
|
+
module Shippy
|
|
5
|
+
module Docker
|
|
6
|
+
class Client
|
|
7
|
+
include SSHKit::DSL
|
|
8
|
+
|
|
9
|
+
def initialize(host)
|
|
10
|
+
@host = host
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ensure_network(name)
|
|
14
|
+
on(@host) do
|
|
15
|
+
if test("[ -z \"$(docker network ls -q -f name=#{name})\" ]")
|
|
16
|
+
execute :docker, :network, :create, name
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def compose_up(path, flags: "")
|
|
22
|
+
on(@host) do
|
|
23
|
+
within(path) do
|
|
24
|
+
args = [:docker, :compose, :up, "-d"]
|
|
25
|
+
args.concat(Array(flags)) unless flags.nil? || flags.empty?
|
|
26
|
+
execute(*args)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def compose_down(path)
|
|
32
|
+
on(@host) do
|
|
33
|
+
if test("[ -f #{path.join("docker-compose.yml")} ]")
|
|
34
|
+
within(path) do
|
|
35
|
+
execute :docker, :compose, :down, "--remove-orphans"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def compose_pull(path, stream: false)
|
|
42
|
+
handler = stream ? Shippy::SshStreamHandler.new : nil
|
|
43
|
+
on(@host) do
|
|
44
|
+
within(path) do
|
|
45
|
+
execute :docker, :compose, :pull, interaction_handler: handler
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def compose_restart(path, services = [])
|
|
51
|
+
on(@host) do
|
|
52
|
+
within(path) do
|
|
53
|
+
execute :docker, :compose, :restart, *services
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run_hook(path, service, options, command)
|
|
59
|
+
on(@host) do
|
|
60
|
+
within(path) do
|
|
61
|
+
# Ensure options are treated as args
|
|
62
|
+
args = [:docker, :compose, :run, "--rm"]
|
|
63
|
+
args << options if options && !options.empty?
|
|
64
|
+
args << service
|
|
65
|
+
args << command
|
|
66
|
+
execute(*args)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def logs(path, follow: false)
|
|
72
|
+
cmd_args = [:docker, "compose", "logs"]
|
|
73
|
+
cmd_args << "--follow" if follow
|
|
74
|
+
|
|
75
|
+
on(@host) do
|
|
76
|
+
within(path) do
|
|
77
|
+
if follow
|
|
78
|
+
execute(*cmd_args, interaction_handler: Shippy::SshStreamHandler.new)
|
|
79
|
+
else
|
|
80
|
+
puts capture(*cmd_args)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def status(path)
|
|
87
|
+
on(@host) do
|
|
88
|
+
within(path) do
|
|
89
|
+
puts capture(:docker, :compose, :ps, "-a")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Shippy
|
|
2
|
+
module Docker
|
|
3
|
+
class System
|
|
4
|
+
include SSHKit::DSL
|
|
5
|
+
|
|
6
|
+
def initialize(host)
|
|
7
|
+
@host = host
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def prune_images(hours_until:)
|
|
11
|
+
on(@host) do
|
|
12
|
+
execute :docker, :image, :prune, "--all", "--force", "--filter", "until=#{hours_until}h"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def prune_containers(hours_until:)
|
|
17
|
+
on(@host) do
|
|
18
|
+
execute :docker, :container, :prune, "--force", "--filter", "until=#{hours_until}h"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Shippy
|
|
2
|
+
class Maintainer
|
|
3
|
+
IMAGE_RETENTION_HOURS = 24 * 7 # 7 days
|
|
4
|
+
CONTAINER_RETENTION_HOURS = 24 * 3 # 3 days
|
|
5
|
+
|
|
6
|
+
def initialize(host, ui: nil)
|
|
7
|
+
@host = host
|
|
8
|
+
@ui = ui
|
|
9
|
+
@docker = Shippy::Docker::System.new(host)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def clean_all
|
|
13
|
+
clean_containers
|
|
14
|
+
clean_images
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def clean_images(hours: IMAGE_RETENTION_HOURS)
|
|
18
|
+
notify("Pruning images older than #{hours} hours...")
|
|
19
|
+
@docker.prune_images(hours_until: hours)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def clean_containers(hours: CONTAINER_RETENTION_HOURS)
|
|
23
|
+
notify("Pruning stopped containers older than #{hours} hours...")
|
|
24
|
+
@docker.prune_containers(hours_until: hours)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def notify(message)
|
|
30
|
+
@ui&.say(message, :yellow)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "sshkit"
|
|
2
|
+
require "sshkit/dsl"
|
|
3
|
+
|
|
4
|
+
module Shippy
|
|
5
|
+
module Remote
|
|
6
|
+
class App
|
|
7
|
+
include SSHKit::DSL
|
|
8
|
+
|
|
9
|
+
def initialize(host, app_name)
|
|
10
|
+
@host = host
|
|
11
|
+
@app_name = app_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def prepare_directories
|
|
15
|
+
path = current_path
|
|
16
|
+
on(@host) { execute :mkdir, "-p", path }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def upload_release(local_archive_path)
|
|
20
|
+
source_dir = current_path.join("..")
|
|
21
|
+
|
|
22
|
+
on(@host) do
|
|
23
|
+
remote_tar = capture("mktemp").strip
|
|
24
|
+
upload! local_archive_path, remote_tar
|
|
25
|
+
|
|
26
|
+
execute :tar, "-xzpf", remote_tar, "-C", source_dir
|
|
27
|
+
|
|
28
|
+
execute :rm, remote_tar
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def backup_current_release
|
|
33
|
+
ts = Time.now.to_i.to_s
|
|
34
|
+
timestamp_dir = backups_path.join(ts)
|
|
35
|
+
current = current_path
|
|
36
|
+
|
|
37
|
+
on(@host) do
|
|
38
|
+
if test("[ -d #{current} ]")
|
|
39
|
+
execute :mkdir, "-p", timestamp_dir
|
|
40
|
+
execute :mv, current, timestamp_dir
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
timestamp_dir.join(@app_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def prune_old_releases(keep: 5)
|
|
48
|
+
base = backups_path
|
|
49
|
+
|
|
50
|
+
on(@host) do
|
|
51
|
+
if test("[ -d #{base} ]")
|
|
52
|
+
releases = capture(:ls, "-1", base).split
|
|
53
|
+
|
|
54
|
+
if releases.count > keep
|
|
55
|
+
to_delete = releases - releases.last(keep)
|
|
56
|
+
|
|
57
|
+
if to_delete.any?
|
|
58
|
+
paths = to_delete.map { |r| base.join(r).to_s }
|
|
59
|
+
execute :rm, "-rf", *paths
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def current_path
|
|
67
|
+
SHIPPY.deploy_path.join("apps", @app_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def backups_path
|
|
71
|
+
SHIPPY.deploy_path.join("backups", @app_name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Shippy
|
|
2
|
+
class SshStreamHandler
|
|
3
|
+
def on_data(command, stream_name, data, channel)
|
|
4
|
+
# command : The SSHKit::Command object
|
|
5
|
+
# stream_name : :stdout or :stderr
|
|
6
|
+
# data : The raw string chunk (e.g. "Pulling layer 1...\n")
|
|
7
|
+
# channel : The SSH channel (can be used to send input back)
|
|
8
|
+
|
|
9
|
+
print data
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/shippy/version.rb
CHANGED
data/lib/shippy.rb
CHANGED
|
@@ -18,6 +18,13 @@ require_relative "shippy/compose"
|
|
|
18
18
|
require_relative "shippy/service"
|
|
19
19
|
require_relative "shippy/repo"
|
|
20
20
|
require_relative "shippy/compiler"
|
|
21
|
+
require_relative "shippy/builder"
|
|
22
|
+
require_relative "shippy/deployer"
|
|
23
|
+
require_relative "shippy/maintainer"
|
|
24
|
+
require_relative "shippy/ssh_stream_handler"
|
|
25
|
+
require_relative "shippy/remote/app"
|
|
26
|
+
require_relative "shippy/docker/client"
|
|
27
|
+
require_relative "shippy/docker/system"
|
|
21
28
|
|
|
22
29
|
module Shippy
|
|
23
30
|
class Error < StandardError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shippy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marius Bobin
|
|
@@ -147,6 +147,20 @@ dependencies:
|
|
|
147
147
|
- - "~>"
|
|
148
148
|
- !ruby/object:Gem::Version
|
|
149
149
|
version: '1.3'
|
|
150
|
+
- !ruby/object:Gem::Dependency
|
|
151
|
+
name: irb
|
|
152
|
+
requirement: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - ">="
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: '0'
|
|
157
|
+
type: :development
|
|
158
|
+
prerelease: false
|
|
159
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
160
|
+
requirements:
|
|
161
|
+
- - ">="
|
|
162
|
+
- !ruby/object:Gem::Version
|
|
163
|
+
version: '0'
|
|
150
164
|
email:
|
|
151
165
|
- marius@mbobin.me
|
|
152
166
|
executables:
|
|
@@ -158,6 +172,7 @@ files:
|
|
|
158
172
|
- README.md
|
|
159
173
|
- bin/shippy
|
|
160
174
|
- lib/shippy.rb
|
|
175
|
+
- lib/shippy/builder.rb
|
|
161
176
|
- lib/shippy/cli.rb
|
|
162
177
|
- lib/shippy/cli/app.rb
|
|
163
178
|
- lib/shippy/cli/base.rb
|
|
@@ -175,10 +190,16 @@ files:
|
|
|
175
190
|
- lib/shippy/compiler.rb
|
|
176
191
|
- lib/shippy/compose.rb
|
|
177
192
|
- lib/shippy/config.rb
|
|
193
|
+
- lib/shippy/deployer.rb
|
|
194
|
+
- lib/shippy/docker/client.rb
|
|
195
|
+
- lib/shippy/docker/system.rb
|
|
196
|
+
- lib/shippy/maintainer.rb
|
|
197
|
+
- lib/shippy/remote/app.rb
|
|
178
198
|
- lib/shippy/repo.rb
|
|
179
199
|
- lib/shippy/secrets.rb
|
|
180
200
|
- lib/shippy/secrets_manager.rb
|
|
181
201
|
- lib/shippy/service.rb
|
|
202
|
+
- lib/shippy/ssh_stream_handler.rb
|
|
182
203
|
- lib/shippy/version.rb
|
|
183
204
|
homepage: https://mbobin.me/shippy
|
|
184
205
|
licenses:
|